mirror of
https://github.com/ccmjga/zhilu-admin
synced 2026-04-08 14:37:38 +00:00
重构组件结构,优化导入路径并移除不必要的组件,新增多个模态框组件以支持部门、角色、权限和用户管理功能
This commit is contained in:
55
frontend/src/components/layout/Breadcrumbs.vue
Normal file
55
frontend/src/components/layout/Breadcrumbs.vue
Normal file
@@ -0,0 +1,55 @@
|
||||
<template>
|
||||
<nav class="flex mb-4" aria-label="Breadcrumb">
|
||||
<ol class="inline-flex items-center space-x-1 sm:space-x-2 text-sm">
|
||||
<li class="inline-flex items-center">
|
||||
<RouterLink :to="Routes.HOME.fullPath()"
|
||||
class="inline-flex items-center font-medium text-gray-500 hover:text-blue-600">
|
||||
<svg class="w-3.5 h-3.5 mr-1.5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor"
|
||||
viewBox="0 0 20 20">
|
||||
<path
|
||||
d="m19.707 9.293-2-2-7-7a1 1 0 0 0-1.414 0l-7 7-2 2a1 1 0 0 0 1.414 1.414L2 10.414V18a2 2 0 0 0 2 2h3a1 1 0 0 0 1-1v-4a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v4a1 1 0 0 0 1 1h3a2 2 0 0 0 2-2v-7.586l.293.293a1 1 0 0 0 1.414-1.414Z" />
|
||||
</svg>
|
||||
首页
|
||||
</RouterLink>
|
||||
</li>
|
||||
<li v-for="(item, index) in breadcrumbs" :key="index">
|
||||
<div class="flex items-center">
|
||||
<svg class="w-3 h-3 text-gray-400 mx-1.5 sm:mx-2" aria-hidden="true" xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none" viewBox="0 0 6 10">
|
||||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="m1 9 4-4-4-4" />
|
||||
</svg>
|
||||
<RouterLink v-if="item.route" :to="item.route" class="font-medium text-gray-500 hover:text-blue-600 truncate">
|
||||
{{ item.name }}
|
||||
</RouterLink>
|
||||
<span v-else class="font-medium text-gray-500 truncate">{{ item.name }}</span>
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Routes } from "@/router/constants";
|
||||
import { computed } from "vue";
|
||||
import type { RouteLocationRaw } from "vue-router";
|
||||
|
||||
interface BreadcrumbItem {
|
||||
name: string;
|
||||
route?: RouteLocationRaw;
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
names: string[];
|
||||
routes?: RouteLocationRaw[];
|
||||
}>();
|
||||
|
||||
const breadcrumbs = computed<BreadcrumbItem[]>(() => {
|
||||
return props.names.map((name, index) => {
|
||||
return {
|
||||
name,
|
||||
route: props.routes?.[index],
|
||||
};
|
||||
});
|
||||
});
|
||||
</script>
|
||||
38
frontend/src/components/layout/Dashboard.vue
Normal file
38
frontend/src/components/layout/Dashboard.vue
Normal file
@@ -0,0 +1,38 @@
|
||||
<template>
|
||||
<Headbar :changeAssistantVisible="changeAssistantVisible" :onSidebarToggle="toggleSidebar"></Headbar>
|
||||
<Sidebar ref="sidebarRef" @menu-click="handleMenuClick"></Sidebar>
|
||||
<div class="flex flex-row h-[calc(100vh-3.5rem)] mt-14">
|
||||
<article class="flex-1 sm:ml-44 overflow-y-auto">
|
||||
<RouterView></RouterView>
|
||||
</article>
|
||||
<Assistant v-if="isAssistantVisible"></Assistant>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import { RouterView } from "vue-router";
|
||||
import Assistant from "@/components/common/Assistant.vue";
|
||||
import Headbar from "./Headbar.vue";
|
||||
import Sidebar from "./Sidebar.vue";
|
||||
|
||||
const isAssistantVisible = ref(false);
|
||||
const sidebarRef = ref();
|
||||
|
||||
const changeAssistantVisible = () => {
|
||||
isAssistantVisible.value = !isAssistantVisible.value;
|
||||
};
|
||||
|
||||
const toggleSidebar = () => {
|
||||
if (sidebarRef.value) {
|
||||
sidebarRef.value.toggleSidebar();
|
||||
}
|
||||
};
|
||||
|
||||
const handleMenuClick = () => {
|
||||
if (sidebarRef.value) {
|
||||
sidebarRef.value.closeSidebar();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
130
frontend/src/components/layout/Headbar.vue
Normal file
130
frontend/src/components/layout/Headbar.vue
Normal file
@@ -0,0 +1,130 @@
|
||||
<template>
|
||||
<nav class="fixed top-0 w-full bg-white border-b border-gray-200 z-40">
|
||||
<div class="px-3 py-3 lg:px-5 lg:pl-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center justify-start rtl:justify-end">
|
||||
<button type="button" @click="handleSidebarToggle"
|
||||
class="inline-flex items-center p-2 text-sm text-gray-500 rounded-lg sm:hidden hover:bg-gray-100">
|
||||
<span class="sr-only">Open sidebar</span>
|
||||
<svg class="w-6 h-6" aria-hidden="true" fill="currentColor" viewBox="0 0 20 20"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path clip-rule="evenodd" fill-rule="evenodd"
|
||||
d="M2 4.75A.75.75 0 012.75 4h14.5a.75.75 0 010 1.5H2.75A.75.75 0 012 4.75zm0 10.5a.75.75 0 01.75-.75h7.5a.75.75 0 010 1.5h-7.5a.75.75 0 01-.75-.75zM2 10a.75.75 0 01.75-.75h14.5a.75.75 0 010 1.5H2.75A.75.75 0 012 10z">
|
||||
</path>
|
||||
</svg>
|
||||
</button>
|
||||
<a href="https://github.com/ccmjga/zhilu-admin" target="_blank" class="flex items-center ms-2 md:me-24 ">
|
||||
<img class="me-3" src="/logo.svg" alt="logo">
|
||||
<span class="self-center text-lg sm:text-xl md:text-2xl font-semibold whitespace-nowrap">知路后台管理</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2 sm:space-x-3">
|
||||
<a href="https://github.com/ccmjga/zhilu-admin" target="_blank"
|
||||
class="hidden sm:flex items-center border rounded-sm border-gray-300">
|
||||
<span class="bg-gray-200 rounded-r-none border-r border-r-gray-300">
|
||||
<svg class="me-0.5 inline pl-1.5 pb-1 w-6 h-6 text-gray-800 bg-gray-200" aria-hidden="true"
|
||||
xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
|
||||
<path stroke="currentColor" stroke-width="2"
|
||||
d="M11.083 5.104c.35-.8 1.485-.8 1.834 0l1.752 4.022a1 1 0 0 0 .84.597l4.463.342c.9.069 1.255 1.2.556 1.771l-3.33 2.723a1 1 0 0 0-.337 1.016l1.03 4.119c.214.858-.71 1.552-1.474 1.106l-3.913-2.281a1 1 0 0 0-1.008 0L7.583 20.8c-.764.446-1.688-.248-1.474-1.106l1.03-4.119A1 1 0 0 0 6.8 14.56l-3.33-2.723c-.698-.571-.342-1.702.557-1.771l4.462-.342a1 1 0 0 0 .84-.597l1.753-4.022Z" />
|
||||
</svg>
|
||||
<span class="text-sm pl-0.5 pr-2 font-medium">Star</span>
|
||||
</span>
|
||||
<span class="text-sm py-0.5 px-2 font-medium">0.2k</span>
|
||||
</a>
|
||||
<button class="cursor-pointer pt-1" @click="changeAssistantVisible">
|
||||
<AiChatIcon />
|
||||
</button>
|
||||
<div class="flex items-center ms-2 sm:ms-3">
|
||||
<div>
|
||||
<button type="button" id="dropdown-button" class="flex text-sm rounded-full cursor-pointer"
|
||||
aria-expanded="false" data-dropdown-toggle="dropdown-user">
|
||||
<span class="sr-only">打开用户菜单</span>
|
||||
<Avatar :src="user.avatar" size="sm" />
|
||||
</button>
|
||||
</div>
|
||||
<div class="z-50 hidden my-4 text-base list-none bg-white divide-y divide-gray-100 rounded-sm shadow-sm "
|
||||
id="dropdown-user">
|
||||
<div class="px-4 py-3" role="none">
|
||||
<p class="text-sm font-medium text-gray-900 truncate " role="none">
|
||||
{{ user.username }}
|
||||
</p>
|
||||
</div>
|
||||
<ul class="py-1" role="none">
|
||||
<li>
|
||||
<button @click="() => {
|
||||
userDropDownMenu?.toggle()
|
||||
router.push(Routes.SETTINGS.fullPath())
|
||||
}" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 "
|
||||
role="menuitem">Settings</button>
|
||||
</li>
|
||||
<li>
|
||||
<button @click="() => {
|
||||
userDropDownMenu?.toggle()
|
||||
router.push(Routes.USERVIEW.fullPath())
|
||||
}" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 "
|
||||
role="menuitem">Dashboard</button>
|
||||
</li>
|
||||
<li>
|
||||
<button @click="handleLogoutClick"
|
||||
class="flex items-center space-x-1 px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 "
|
||||
role="menuitem">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
|
||||
class="lucide lucide-log-out-icon w-4 h-4 lucide-log-out">
|
||||
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" />
|
||||
<polyline points="16 17 21 12 16 7" />
|
||||
<line x1="21" x2="9" y1="12" y2="12" />
|
||||
</svg><span>
|
||||
Sign out
|
||||
</span>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { AiChatIcon } from "@/components/icons";
|
||||
import Avatar from "@/components/ui/Avatar.vue";
|
||||
import useUserAuth from "@/composables/auth/useUserAuth";
|
||||
import useUserStore from "@/composables/store/useUserStore";
|
||||
import { Routes } from "@/router/constants";
|
||||
import { Dropdown, type DropdownInterface, initFlowbite } from "flowbite";
|
||||
import { onMounted, ref } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
|
||||
const props = defineProps<{
|
||||
changeAssistantVisible: () => void;
|
||||
onSidebarToggle: () => void;
|
||||
}>();
|
||||
|
||||
const handleSidebarToggle = () => {
|
||||
props.onSidebarToggle();
|
||||
};
|
||||
|
||||
const userDropDownMenu = ref<DropdownInterface>();
|
||||
|
||||
const { user } = useUserStore();
|
||||
const { signOut } = useUserAuth();
|
||||
const router = useRouter();
|
||||
const handleLogoutClick = () => {
|
||||
signOut();
|
||||
router.push(Routes.LOGIN.path);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
initFlowbite();
|
||||
const $dropdownUser = document.getElementById("dropdown-user");
|
||||
const $dropdownButton = document.getElementById("dropdown-button");
|
||||
userDropDownMenu.value = new Dropdown(
|
||||
$dropdownUser,
|
||||
$dropdownButton,
|
||||
{},
|
||||
{ id: "dropdownMenu", override: true },
|
||||
);
|
||||
});
|
||||
</script>
|
||||
148
frontend/src/components/layout/Sidebar.vue
Normal file
148
frontend/src/components/layout/Sidebar.vue
Normal file
@@ -0,0 +1,148 @@
|
||||
<template>
|
||||
<aside id="logo-sidebar"
|
||||
class="fixed top-0 left-0 px-1 w-44 min-h-screen overflow-y-auto pt-20 transform transition-transform duration-300 ease-in-out bg-white border-r border-gray-200"
|
||||
:class="[
|
||||
isDrawerVisible ? 'translate-x-0' : '-translate-x-full sm:translate-x-0',
|
||||
isDrawerVisible ? 'z-30' : ''
|
||||
]" aria-label="Sidebar">
|
||||
<div class="h-full px-3 pb-4 overflow-y-auto bg-white">
|
||||
<ul class="space-y-2 font-medium">
|
||||
<li v-for="item in menuItems" :key="item.path">
|
||||
<RouterLink :to="item.path"
|
||||
class="flex items-center p-2 gap-x-2 text-gray-900 rounded-lg hover:bg-gray-100 group"
|
||||
:class="{ 'bg-gray-100': isActive(item.path) }" @click="handleMenuClick">
|
||||
<component :is="item.icon"
|
||||
class="shrink-0 text-gray-500 transition duration-75 group-hover:text-gray-900" />
|
||||
<span>{{ item.title }}</span>
|
||||
</RouterLink>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- 遮罩层 -->
|
||||
<div class="fixed inset-0 bg-gray-900/50 transition-all duration-300 sm:hidden z-20" :class="[
|
||||
isDrawerVisible ? 'opacity-100' : 'opacity-0 pointer-events-none'
|
||||
]" @click="closeSidebar">
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Routes } from "@/router/constants";
|
||||
import { initFlowbite } from "flowbite";
|
||||
import { onMounted, ref } from "vue";
|
||||
import { RouterLink, useRoute } from "vue-router";
|
||||
|
||||
import {
|
||||
DepartmentIcon,
|
||||
LlmConfigIcon,
|
||||
PermissionIcon,
|
||||
PositionIcon,
|
||||
RoleIcon,
|
||||
SchedulerIcon,
|
||||
SettingsIcon,
|
||||
UsersIcon,
|
||||
} from "@/components/icons";
|
||||
|
||||
const isDrawerVisible = ref(false);
|
||||
const emit = defineEmits(["menu-click"]);
|
||||
|
||||
// 菜单点击处理
|
||||
const handleMenuClick = () => {
|
||||
emit("menu-click");
|
||||
};
|
||||
|
||||
const toggleSidebar = () => {
|
||||
isDrawerVisible.value = !isDrawerVisible.value;
|
||||
};
|
||||
|
||||
const openSidebar = () => {
|
||||
isDrawerVisible.value = true;
|
||||
};
|
||||
|
||||
const closeSidebar = () => {
|
||||
isDrawerVisible.value = false;
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
toggleSidebar,
|
||||
openSidebar,
|
||||
closeSidebar,
|
||||
isDrawerVisible,
|
||||
});
|
||||
|
||||
// 菜单配置
|
||||
const menuItems = [
|
||||
{
|
||||
title: "用户管理",
|
||||
path: Routes.USERVIEW.fullPath(),
|
||||
icon: UsersIcon,
|
||||
},
|
||||
{
|
||||
title: "角色管理",
|
||||
path: Routes.ROLEVIEW.fullPath(),
|
||||
icon: RoleIcon,
|
||||
},
|
||||
{
|
||||
title: "权限管理",
|
||||
path: Routes.PERMISSIONVIEW.fullPath(),
|
||||
icon: PermissionIcon,
|
||||
},
|
||||
{
|
||||
title: "部门管理",
|
||||
path: Routes.DEPARTMENTVIEW.fullPath(),
|
||||
icon: DepartmentIcon,
|
||||
},
|
||||
{
|
||||
title: "岗位管理",
|
||||
path: Routes.POSITIONVIEW.fullPath(),
|
||||
icon: PositionIcon,
|
||||
},
|
||||
{
|
||||
title: "个人中心",
|
||||
path: Routes.SETTINGS.fullPath(),
|
||||
icon: SettingsIcon,
|
||||
},
|
||||
{
|
||||
title: "定时任务",
|
||||
path: Routes.SCHEDULERVIEW.fullPath(),
|
||||
icon: SchedulerIcon,
|
||||
},
|
||||
{
|
||||
title: "大模型管理",
|
||||
path: Routes.LLMCONFIGVIEW.fullPath(),
|
||||
icon: LlmConfigIcon,
|
||||
},
|
||||
];
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
const isActive = (path: string) => {
|
||||
return route.path === path;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
initFlowbite();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.invisible {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.visible {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
/* 添加移动端样式 */
|
||||
@media (max-width: 640px) {
|
||||
.translate-x-0 {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.-translate-x-full {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user