mirror of
https://github.com/fastapiadmin/FastapiAdmin.git
synced 2026-10-08 18:49:09 +00:00
@@ -1,3 +1,5 @@
|
|||||||
|
from typing import Any
|
||||||
|
|
||||||
from app.api.v1.module_system.auth.schema import AuthSchema
|
from app.api.v1.module_system.auth.schema import AuthSchema
|
||||||
from app.core.base_schema import BatchSetAvailable
|
from app.core.base_schema import BatchSetAvailable
|
||||||
from app.core.exceptions import CustomException
|
from app.core.exceptions import CustomException
|
||||||
@@ -84,7 +86,10 @@ class MenuService:
|
|||||||
返回:
|
返回:
|
||||||
- dict: 创建的菜单对象。
|
- dict: 创建的菜单对象。
|
||||||
"""
|
"""
|
||||||
menu = await MenuCRUD(auth).get(name=data.name)
|
search: dict[str, Any] = {"title": data.title}
|
||||||
|
if data.parent_id is not None:
|
||||||
|
search["parent_id"] = data.parent_id
|
||||||
|
menu = await MenuCRUD(auth).get(**search)
|
||||||
if menu:
|
if menu:
|
||||||
raise CustomException(msg="创建失败,该菜单已存在")
|
raise CustomException(msg="创建失败,该菜单已存在")
|
||||||
|
|
||||||
@@ -108,15 +113,18 @@ class MenuService:
|
|||||||
menu = await MenuCRUD(auth).get_by_id_crud(id=id)
|
menu = await MenuCRUD(auth).get_by_id_crud(id=id)
|
||||||
if not menu:
|
if not menu:
|
||||||
raise CustomException(msg="更新失败,该菜单不存在")
|
raise CustomException(msg="更新失败,该菜单不存在")
|
||||||
exist_menu = await MenuCRUD(auth).get(name=data.name)
|
search: dict[str, Any] = {"title": data.title}
|
||||||
|
if data.parent_id is not None:
|
||||||
|
search["parent_id"] = data.parent_id
|
||||||
|
exist_menu = await MenuCRUD(auth).get(**search)
|
||||||
if exist_menu and exist_menu.id != id:
|
if exist_menu and exist_menu.id != id:
|
||||||
raise CustomException(msg="更新失败,菜单名称重复")
|
raise CustomException(msg="更新失败,菜单标题重复")
|
||||||
|
|
||||||
if data.parent_id:
|
if data.parent_id:
|
||||||
parent_menu = await MenuCRUD(auth).get_by_id_crud(id=data.parent_id)
|
parent_menu = await MenuCRUD(auth).get_by_id_crud(id=data.parent_id)
|
||||||
if not parent_menu:
|
if not parent_menu:
|
||||||
raise CustomException(msg="更新失败,父级菜单不存在")
|
raise CustomException(msg="更新失败,父级菜单不存在")
|
||||||
data.parent_name = parent_menu.name
|
data.parent_name = parent_menu.title
|
||||||
new_menu = await MenuCRUD(auth).update(id=id, data=data)
|
new_menu = await MenuCRUD(auth).update(id=id, data=data)
|
||||||
|
|
||||||
await cls.set_menu_available_service(
|
await cls.set_menu_available_service(
|
||||||
|
|||||||
+6
-6
@@ -7,9 +7,9 @@ services:
|
|||||||
restart: always
|
restart: always
|
||||||
environment:
|
environment:
|
||||||
TZ: "Asia/Shanghai"
|
TZ: "Asia/Shanghai"
|
||||||
MYSQL_ROOT_PASSWORD: "FastApi123abc"
|
MYSQL_ROOT_PASSWORD: "fastapiadmin"
|
||||||
MYSQL_DATABASE: "fastapiadmin"
|
MYSQL_DATABASE: "fastapiadmin"
|
||||||
MYSQL_PASSWORD: "FastApi123abc"
|
MYSQL_PASSWORD: "fastapiadmin"
|
||||||
MYSQL_USER: "fastapiadmin"
|
MYSQL_USER: "fastapiadmin"
|
||||||
MYSQL_TZINFO_TO_SYS_TABLES: 1 # 初始化MySQL时区表,解决时差问题
|
MYSQL_TZINFO_TO_SYS_TABLES: 1 # 初始化MySQL时区表,解决时差问题
|
||||||
ports:
|
ports:
|
||||||
@@ -24,7 +24,7 @@ services:
|
|||||||
- app_network
|
- app_network
|
||||||
# 健康检查:检测MySQL是否真正就绪,避免后端启动早于MySQL
|
# 健康检查:检测MySQL是否真正就绪,避免后端启动早于MySQL
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-pFastApi123abc"]
|
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-pfastapiadmin"]
|
||||||
interval: 5s # 每5秒检测一次
|
interval: 5s # 每5秒检测一次
|
||||||
timeout: 30s # 超时时间30秒
|
timeout: 30s # 超时时间30秒
|
||||||
retries: 10 # 重试10次,失败则认为容器未就绪
|
retries: 10 # 重试10次,失败则认为容器未就绪
|
||||||
@@ -42,7 +42,7 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
# 挂载Redis配置文件,实现自定义配置
|
# 挂载Redis配置文件,实现自定义配置
|
||||||
- ./devops/redis/data:/data
|
- ./devops/redis/data:/data
|
||||||
command: redis-server --requirepass FastApi123abc
|
command: redis-server --requirepass fastapiadmin
|
||||||
networks:
|
networks:
|
||||||
- app_network
|
- app_network
|
||||||
|
|
||||||
@@ -57,9 +57,9 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
TZ: "Asia/Shanghai"
|
TZ: "Asia/Shanghai"
|
||||||
DATABASE_HOST: "mysql"
|
DATABASE_HOST: "mysql"
|
||||||
DATABASE_PASSWORD: "FastApi123abc"
|
DATABASE_PASSWORD: "fastapiadmin"
|
||||||
REDIS_HOST: "redis"
|
REDIS_HOST: "redis"
|
||||||
REDIS_PASSWORD: "FastApi123abc"
|
REDIS_PASSWORD: "fastapiadmin"
|
||||||
ports:
|
ports:
|
||||||
- "8001:8001"
|
- "8001:8001"
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
@@ -18,14 +18,10 @@
|
|||||||
? variables['menu-active-text']
|
? variables['menu-active-text']
|
||||||
: undefined
|
: undefined
|
||||||
"
|
"
|
||||||
@select="handleMenuSelect"
|
@select="handleTopMenuSelect"
|
||||||
>
|
>
|
||||||
<el-menu-item v-for="menuItem in processedTopMenus" :key="menuItem.path" :index="menuItem.path">
|
<el-menu-item v-for="item in topMenuItems" :key="item.path" :index="item.path">
|
||||||
<MenuItemContent
|
<MenuItemContent v-if="item.meta" :icon="item.meta.icon" :title="item.meta.title" />
|
||||||
v-if="menuItem.meta"
|
|
||||||
:icon="menuItem.meta.icon"
|
|
||||||
:title="menuItem.meta.title"
|
|
||||||
/>
|
|
||||||
</el-menu-item>
|
</el-menu-item>
|
||||||
</el-menu>
|
</el-menu>
|
||||||
</template>
|
</template>
|
||||||
@@ -57,7 +53,7 @@ const sidebarColorScheme = computed(() => settingsStore.sidebarColorScheme);
|
|||||||
const topMenus = ref<RouteRecordRaw[]>([]);
|
const topMenus = ref<RouteRecordRaw[]>([]);
|
||||||
|
|
||||||
// 处理后的顶部菜单列表 - 智能显示唯一子菜单的标题
|
// 处理后的顶部菜单列表 - 智能显示唯一子菜单的标题
|
||||||
const processedTopMenus = computed(() => {
|
const topMenuItems = computed(() => {
|
||||||
return topMenus.value.map((route) => {
|
return topMenus.value.map((route) => {
|
||||||
// 如果路由设置了 alwaysShow=true,或者没有子菜单,直接返回原路由
|
// 如果路由设置了 alwaysShow=true,或者没有子菜单,直接返回原路由
|
||||||
if (route.meta?.alwaysShow || !route.children || route.children.length === 0) {
|
if (route.meta?.alwaysShow || !route.children || route.children.length === 0) {
|
||||||
@@ -89,7 +85,7 @@ const processedTopMenus = computed(() => {
|
|||||||
* 处理菜单点击事件,切换顶部菜单并加载对应的左侧菜单
|
* 处理菜单点击事件,切换顶部菜单并加载对应的左侧菜单
|
||||||
* @param routePath 点击的菜单路径
|
* @param routePath 点击的菜单路径
|
||||||
*/
|
*/
|
||||||
const handleMenuSelect = (routePath: string) => {
|
const handleTopMenuSelect = (routePath: string) => {
|
||||||
updateMenuState(routePath);
|
updateMenuState(routePath);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -274,7 +274,7 @@ function logout() {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
min-width: 44px; /* 增加最小点击区域到44px,符合人机交互标准 */
|
min-width: 44px; /* 增加最小点击区域到44px,符合人机交互标准 */
|
||||||
height: 100%;
|
height: 44px;
|
||||||
padding: 0 8px;
|
padding: 0 8px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@@ -315,7 +315,7 @@ function logout() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: rgba(0, 0, 0, 0.04);
|
background: var(--el-fill-color-light);
|
||||||
|
|
||||||
:deep([class^="i-svg:"]) {
|
:deep([class^="i-svg:"]) {
|
||||||
color: var(--el-color-primary);
|
color: var(--el-color-primary);
|
||||||
@@ -368,20 +368,20 @@ function logout() {
|
|||||||
.navbar-actions--white-text {
|
.navbar-actions--white-text {
|
||||||
.navbar-actions__item {
|
.navbar-actions__item {
|
||||||
:deep([class^="i-svg:"]) {
|
:deep([class^="i-svg:"]) {
|
||||||
color: rgba(255, 255, 255, 0.85);
|
color: color-mix(in srgb, var(--el-color-white) 85%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: rgba(255, 255, 255, 0.1);
|
background: color-mix(in srgb, var(--el-color-white) 10%, transparent);
|
||||||
|
|
||||||
:deep([class^="i-svg:"]) {
|
:deep([class^="i-svg:"]) {
|
||||||
color: #fff;
|
color: var(--el-color-white);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-profile__name {
|
.user-profile__name {
|
||||||
color: rgba(255, 255, 255, 0.85);
|
color: color-mix(in srgb, var(--el-color-white) 85%, transparent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,14 +3,20 @@
|
|||||||
<!-- 顶部菜单栏 -->
|
<!-- 顶部菜单栏 -->
|
||||||
<div class="layout__header">
|
<div class="layout__header">
|
||||||
<div class="layout__header-left">
|
<div class="layout__header-left">
|
||||||
<!-- Logo -->
|
<!-- Logo区域 -->
|
||||||
<AppLogo v-if="isShowLogo" :collapse="isLogoCollapsed" />
|
<div v-if="isShowLogo" class="layout__header-logo">
|
||||||
<!-- 菜单 -->
|
<AppLogo :collapse="isLogoCollapsed" />
|
||||||
<BasicMenu :data="routes" menu-mode="horizontal" base-path="" />
|
</div>
|
||||||
</div>
|
|
||||||
<!-- 操作按钮 -->
|
<!-- 顶部菜单区域 -->
|
||||||
<div class="layout__header-right">
|
<div class="layout__header-menu">
|
||||||
<NavbarActions />
|
<BasicMenu :data="topMenuItems" menu-mode="horizontal" base-path="" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 右侧操作区域 -->
|
||||||
|
<div class="layout__header-right">
|
||||||
|
<NavbarActions />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -26,7 +32,7 @@
|
|||||||
import { computed } from "vue";
|
import { computed } from "vue";
|
||||||
import { useWindowSize } from "@vueuse/core";
|
import { useWindowSize } from "@vueuse/core";
|
||||||
import { useLayout } from "../composables/useLayout";
|
import { useLayout } from "../composables/useLayout";
|
||||||
import { useLayoutMenu } from "../composables/useLayoutMenu";
|
import { usePermissionStore } from "@/store";
|
||||||
import BaseLayout from "./BaseLayout.vue";
|
import BaseLayout from "./BaseLayout.vue";
|
||||||
import AppLogo from "../components/AppLogo/index.vue";
|
import AppLogo from "../components/AppLogo/index.vue";
|
||||||
import BasicMenu from "../components/Menu/BasicMenu.vue";
|
import BasicMenu from "../components/Menu/BasicMenu.vue";
|
||||||
@@ -38,7 +44,10 @@ import AppMain from "../components/AppMain/index.vue";
|
|||||||
const { isShowTagsView, isShowLogo } = useLayout();
|
const { isShowTagsView, isShowLogo } = useLayout();
|
||||||
|
|
||||||
// 菜单相关
|
// 菜单相关
|
||||||
const { routes } = useLayoutMenu();
|
const permissionStore = usePermissionStore();
|
||||||
|
const topMenuItems = computed(() => {
|
||||||
|
return permissionStore.routes.filter((item) => !item.meta?.hidden);
|
||||||
|
});
|
||||||
|
|
||||||
// 响应式窗口尺寸
|
// 响应式窗口尺寸
|
||||||
const { width } = useWindowSize();
|
const { width } = useWindowSize();
|
||||||
@@ -64,63 +73,80 @@ const isLogoCollapsed = computed(() => width.value < 768);
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
min-width: 0; // 允许flex收缩
|
min-width: 0;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-logo {
|
||||||
|
display: flex;
|
||||||
|
flex-shrink: 0;
|
||||||
|
align-items: center;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
// Logo样式由AppLogo组件的全局样式控制
|
|
||||||
:deep(.logo) {
|
:deep(.logo) {
|
||||||
flex-shrink: 0; // 防止Logo被压缩
|
|
||||||
height: $navbar-height;
|
height: $navbar-height;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&-menu {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
align-items: center;
|
||||||
|
min-width: 0;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
:deep(.el-menu) {
|
||||||
|
height: 100%;
|
||||||
|
background-color: transparent;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-menu--horizontal) {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
height: $navbar-height;
|
||||||
|
overflow: hidden;
|
||||||
|
line-height: $navbar-height;
|
||||||
|
background-color: transparent;
|
||||||
|
border: none;
|
||||||
|
|
||||||
|
.el-menu-item {
|
||||||
|
height: $navbar-height;
|
||||||
|
line-height: $navbar-height;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-sub-menu {
|
||||||
|
.el-sub-menu__title {
|
||||||
|
height: $navbar-height;
|
||||||
|
line-height: $navbar-height;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.has-active-child {
|
||||||
|
.el-sub-menu__title {
|
||||||
|
color: var(--el-color-primary) !important;
|
||||||
|
border-bottom: 2px solid var(--el-color-primary) !important;
|
||||||
|
|
||||||
|
.menu-icon {
|
||||||
|
color: var(--el-color-primary) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-menu--popup {
|
||||||
|
min-width: 160px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&-right {
|
&-right {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-shrink: 0; // 防止操作按钮被压缩
|
flex-shrink: 0;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
padding-left: 12px;
|
padding-left: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 菜单样式
|
|
||||||
:deep(.el-menu--horizontal) {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0; // 允许菜单收缩
|
|
||||||
height: $navbar-height;
|
|
||||||
overflow: hidden; // 防止菜单溢出
|
|
||||||
line-height: $navbar-height;
|
|
||||||
background-color: transparent;
|
|
||||||
border: none;
|
|
||||||
|
|
||||||
.el-menu-item {
|
|
||||||
height: $navbar-height;
|
|
||||||
line-height: $navbar-height;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-sub-menu {
|
|
||||||
.el-sub-menu__title {
|
|
||||||
height: $navbar-height;
|
|
||||||
line-height: $navbar-height;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 父菜单激活状态 - 水平布局专用
|
|
||||||
&.has-active-child {
|
|
||||||
.el-sub-menu__title {
|
|
||||||
color: var(--el-color-primary) !important;
|
|
||||||
border-bottom: 2px solid var(--el-color-primary) !important;
|
|
||||||
|
|
||||||
.menu-icon {
|
|
||||||
color: var(--el-color-primary) !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 修复子菜单弹出位置
|
|
||||||
.el-menu--popup {
|
|
||||||
min-width: 160px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&__main {
|
&__main {
|
||||||
|
|||||||
@@ -457,10 +457,9 @@
|
|||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item label="密码" prop="password">
|
<el-form-item v-if="!formData.id" label="密码" prop="password">
|
||||||
<el-input
|
<el-input
|
||||||
v-model="formData.password"
|
v-model="formData.password"
|
||||||
:readonly="!!formData.id"
|
|
||||||
placeholder="请输入密码"
|
placeholder="请输入密码"
|
||||||
type="password"
|
type="password"
|
||||||
show-password
|
show-password
|
||||||
|
|||||||
Reference in New Issue
Block a user