| | |
| | | <el-menu |
| | | :default-active="active" |
| | | :default-openeds="defaultOpeneds" |
| | | unique-opened |
| | | router |
| | | class="side-menu" |
| | | background-color="#2f63d4" |
| | |
| | | > |
| | | <template #title> |
| | | <el-icon><Document /></el-icon> |
| | | <span>记录与日志</span> |
| | | </template> |
| | | <el-menu-item v-if="auth.hasPermission('appointments:page')" index="/appointments">预约记录</el-menu-item> |
| | | <el-menu-item v-if="auth.hasPermission('approvals:page')" index="/approvals">待办日志</el-menu-item> |
| | | <el-menu-item v-if="auth.hasPermission('exports:page')" index="/exports">导出任务</el-menu-item> |
| | | </el-sub-menu> |
| | | <span>记录与日志</span> |
| | | </template> |
| | | <el-menu-item v-if="auth.hasPermission('appointments:page')" index="/appointments">预约记录</el-menu-item> |
| | | <el-menu-item v-if="auth.hasPermission('approvals:page')" index="/approvals">运维日志</el-menu-item> |
| | | <el-menu-item v-if="auth.hasPermission('exports:page')" index="/exports">操作日志</el-menu-item> |
| | | </el-sub-menu> |
| | | |
| | | <el-sub-menu |
| | | v-if="auth.hasAnyPermission(['sys:users:page', 'sys:roles:page'])" |
| | |
| | | class="route-tab" |
| | | :class="{ active: tab.path === active }" |
| | | @click="goTab(tab.path)" |
| | | @contextmenu.prevent="openTabContextMenu($event, tab.path)" |
| | | > |
| | | <el-icon class="tab-home" v-if="tab.path === '/dashboard'"><House /></el-icon> |
| | | <span>{{ tab.title }}</span> |
| | |
| | | x |
| | | </button> |
| | | </div> |
| | | <div |
| | | v-if="tabContextMenu.visible" |
| | | class="tab-context-menu" |
| | | :style="{ left: `${tabContextMenu.x}px`, top: `${tabContextMenu.y}px` }" |
| | | > |
| | | <button type="button" class="tab-context-item" @click="closeLeftTabs">关闭左边</button> |
| | | <button type="button" class="tab-context-item" @click="closeRightTabs">关闭右边</button> |
| | | <button type="button" class="tab-context-item" @click="closeOtherTabs">关闭其他</button> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | |
| | | </template> |
| | | |
| | | <script setup lang="ts"> |
| | | import { computed, ref, watch } from "vue"; |
| | | import { computed, onBeforeUnmount, onMounted, reactive, ref, watch } from "vue"; |
| | | import { useRoute, useRouter } from "vue-router"; |
| | | import { Document, Grid, House, Operation, Setting, User, UserFilled, Wallet } from "@element-plus/icons-vue"; |
| | | import { useAuthStore } from "../stores/auth"; |
| | |
| | | const router = useRouter(); |
| | | const auth = useAuthStore(); |
| | | const active = computed(() => route.path); |
| | | const defaultOpeneds: string[] = []; |
| | | const defaultOpeneds = computed(() => { |
| | | const path = route.path; |
| | | if (path.startsWith("/system/")) return ["system"]; |
| | | if (path.startsWith("/staff/")) return ["staff"]; |
| | | if (path.startsWith("/region/")) return ["region"]; |
| | | if (path === "/appointments" || path === "/approvals" || path === "/exports") return ["logs"]; |
| | | if (path === "/users" || path.startsWith("/roles")) return ["account"]; |
| | | return []; |
| | | }); |
| | | const tabs = ref<LayoutTab[]>([{ path: "/dashboard", title: "首页", closable: false }]); |
| | | const tabContextMenu = reactive({ |
| | | visible: false, |
| | | x: 0, |
| | | y: 0, |
| | | path: "", |
| | | }); |
| | | const routeTitleMap: Record<string, string> = { |
| | | "/dashboard": "首页", |
| | | "/appointments": "预约记录", |
| | | "/approvals": "待办日志", |
| | | "/approvals": "运维日志", |
| | | "/exports": "操作日志", |
| | | "/system/approval-settings": "审核配置", |
| | | "/system/booking-settings": "预约配置", |
| | | "/system/visit-reasons": "来访事由配置", |
| | |
| | | "/region/pass-areas": "通行区域管理", |
| | | "/region/local-units": "属地单位管理", |
| | | }; |
| | | const hiddenTabRouteNames = new Set(["staff-group-detail", "role-detail"]); |
| | | const nameInitial = computed(() => { |
| | | const n = auth.displayName; |
| | | return n && n.length ? n.slice(0, 1) : "?"; |
| | |
| | | } |
| | | |
| | | function ensureTab() { |
| | | if (typeof route.name === "string" && hiddenTabRouteNames.has(route.name)) { |
| | | return; |
| | | } |
| | | const path = route.path; |
| | | const title = resolveRouteTitle(); |
| | | const idx = tabs.value.findIndex((item) => item.path === path); |
| | |
| | | } |
| | | |
| | | function closeTab(path: string) { |
| | | hideTabContextMenu(); |
| | | const idx = tabs.value.findIndex((item) => item.path === path); |
| | | if (idx < 0) { |
| | | return; |
| | |
| | | router.push(fallback?.path || "/dashboard"); |
| | | } |
| | | |
| | | function openTabContextMenu(event: MouseEvent, path: string) { |
| | | tabContextMenu.visible = true; |
| | | tabContextMenu.x = event.clientX; |
| | | tabContextMenu.y = event.clientY; |
| | | tabContextMenu.path = path; |
| | | } |
| | | |
| | | function hideTabContextMenu() { |
| | | tabContextMenu.visible = false; |
| | | tabContextMenu.path = ""; |
| | | } |
| | | |
| | | function closeTabsByFilter(keep: (tab: LayoutTab, index: number) => boolean) { |
| | | const currentPath = route.path; |
| | | tabs.value = tabs.value.filter((tab, index) => !tab.closable || keep(tab, index)); |
| | | if (!tabs.value.some((tab) => tab.path === currentPath)) { |
| | | const fallback = tabs.value[tabs.value.length - 1] || tabs.value[0]; |
| | | router.push(fallback?.path || "/dashboard"); |
| | | } |
| | | hideTabContextMenu(); |
| | | } |
| | | |
| | | function closeLeftTabs() { |
| | | const targetIndex = tabs.value.findIndex((tab) => tab.path === tabContextMenu.path); |
| | | if (targetIndex < 0) { |
| | | hideTabContextMenu(); |
| | | return; |
| | | } |
| | | closeTabsByFilter((tab, index) => index >= targetIndex || !tab.closable); |
| | | } |
| | | |
| | | function closeRightTabs() { |
| | | const targetIndex = tabs.value.findIndex((tab) => tab.path === tabContextMenu.path); |
| | | if (targetIndex < 0) { |
| | | hideTabContextMenu(); |
| | | return; |
| | | } |
| | | closeTabsByFilter((tab, index) => index <= targetIndex || !tab.closable); |
| | | } |
| | | |
| | | function closeOtherTabs() { |
| | | const targetPath = tabContextMenu.path; |
| | | if (!targetPath) { |
| | | hideTabContextMenu(); |
| | | return; |
| | | } |
| | | closeTabsByFilter((tab) => tab.path === targetPath || !tab.closable); |
| | | } |
| | | |
| | | function handleGlobalClick() { |
| | | hideTabContextMenu(); |
| | | } |
| | | |
| | | watch( |
| | | () => route.path, |
| | | () => { |
| | |
| | | }, |
| | | { immediate: true } |
| | | ); |
| | | |
| | | onMounted(() => { |
| | | window.addEventListener("click", handleGlobalClick); |
| | | }); |
| | | |
| | | onBeforeUnmount(() => { |
| | | window.removeEventListener("click", handleGlobalClick); |
| | | }); |
| | | |
| | | async function logout() { |
| | | try { |
| | |
| | | } |
| | | |
| | | .tabs-bar { |
| | | position: relative; |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 6px; |
| | |
| | | cursor: pointer; |
| | | } |
| | | |
| | | .tab-context-menu { |
| | | position: fixed; |
| | | z-index: 1200; |
| | | min-width: 120px; |
| | | padding: 6px 0; |
| | | border: 1px solid #d8e3fb; |
| | | border-radius: 8px; |
| | | background: #ffffff; |
| | | box-shadow: 0 8px 24px rgba(52, 84, 148, 0.16); |
| | | } |
| | | |
| | | .tab-context-item { |
| | | display: block; |
| | | width: 100%; |
| | | border: 0; |
| | | padding: 8px 14px; |
| | | background: transparent; |
| | | color: #4e5f82; |
| | | font-size: 13px; |
| | | text-align: left; |
| | | cursor: pointer; |
| | | } |
| | | |
| | | .tab-context-item:hover { |
| | | background: #f3f7ff; |
| | | color: #4f74d8; |
| | | } |
| | | |
| | | .main-area { |
| | | background: linear-gradient(180deg, #f3f7ff 0%, #eef3fd 100%); |
| | | padding: 14px; |