- CSDN文库",
"datePublished": "2025-07-28",
"keywords": "
<template>
<div :class="['page-layout', wrapClass, isDark && 'theme-dark']">
<div :class="['page-layout--placeholer', breadMenuHidden && 'no-nav']"></div>
<TopHeader class="page-layout__header" />
<SideBar
:menus
class="page-layout__side"
/>
<div :class="['page-layout__main', { 'no-nav': breadMenuHidden }]">
<div
v-if="!breadMenuHidden"
class="page-layout__main-nav"
>
<BreadMenu />
</div>
<div
class="page-layout__main-view"
:class="{ 'is-mobile': appStore.$state.isMobile }"
>
<div
v-if="pageTitle.visible"
class="page-layout__main-view-title common-page-title"
>
<span> {{ pageTitle.label }}</span>
<a-button
v-if="route?.meta?.backBtn"
type="primary"
@click="handleBack"
>{{ btnLabel }}</a-button
>
</div>
<RouterView v-slot="{ Component }">
<KeepAlive :include="cacheViews">
<component
:is="Component"
:key="routeKey"
/>
</KeepAlive>
</RouterView>
</div>
<!-- TODO: 底部有固定定位元素<MainFixedBar />时占位用 勿删! -->
<div class="page-bar__fixed-placeholer"></div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, onMounted, ref, h, watchEffect, watch, nextTick } from "vue"
import { useAppStore, useMsgStore, useUserStore, useSysConfigStore } from "@/stores"
import TopHeader from "./components/TopHeader.vue"
import SideBar from "./components/side-bar/index.vue"
import BreadMenu from "./components/BreadMenu.vue"
import { onBeforeRouteLeave, useRoute, useRouter } from "vue-router"
import { Button, Notification, type NotificationReturn } from "@arco-design/web-vue"
import type { RecordModel as MessageData } from "@/api/message/api-type"
import { IconRight } from "@arco-design/web-vue/es/icon"
import { ORDER_TRANSFER } from "@/router/order"
import { getSystemConfig } from "@/api/system/index.serv"
import { to } from "await-to-js"
defineOptions({
name: "Layout"
})
const route = useRoute()
const router = useRouter()
const appStore = useAppStore()
const userStore = useUserStore()
const msgStore = useMsgStore()
const sysConfigStore = useSysConfigStore()
const orderMsgReturn = ref<NotificationReturn>()
const breadMenuHidden = ref(false)
const btnLabel = computed(() => {
return window.opener ? "关闭" : route?.meta.backBtn?.label || "关闭"
})
const cacheViews = computed(() => appStore.routeViewCache)
const msgId = computed(() => msgStore.msgNoticeId)
const pageTitle = computed(() => {
const { navTitle, title, titleVisible } = route.meta ?? {}
return {
visible: title && titleVisible !== false,
label: navTitle || title
}
})
const routeKey = computed(() => {
const { name, query = {} } = route
if (Object.keys(query).length) {
return `${name as string}-${JSON.stringify(route.query)}`
}
return name as string
})
const wrapClass = computed(() => {
return appStore.menuStatus ? "menu-collapse" : "menu-expend"
})
const isDark = computed(() => appStore.theme === "dark")
const menus = computed(() => userStore.accessMenu ?? [])
const menuMap = computed(() => userStore.accessMenuMap)
const doOrderLoop = computed(() => {
return [...menuMap.value.values()].some((list) => {
const loopData = list?.meta?.doLoop
if (!loopData) return false
const { code, dependMenu } = loopData
if (code === "order") {
if (!dependMenu) return true
return menuMap.value.has(dependMenu)
} else {
return false
}
})
})
watchEffect(() => {
breadMenuHidden.value = route.meta?.breadMenuVisible === false
})
const goToTransfer = (data: MessageData) => {
router
.push({
name: ORDER_TRANSFER.name
})
.then((res) => {
Notification.remove(msgId.value)
})
}
const showOrderMsg = (data: MessageData) => {
return Notification.info({
icon: () => h("span", { class: ["iconfont icon-file"] }),
title: "您有一个订单需要进行调度",
content: data.msg_body,
id: msgId.value,
closable: true,
duration: 0,
class: "order-msg__tip",
position: "bottomRight",
footer: () =>
h(
Button,
{
type: "text",
onClick: () => goToTransfer(data)
},
["立即前往", h(IconRight)]
)
})
}
watch(
[() => msgStore.msgTargetId, () => sysConfigStore.orderMessageConfig.orderNoticeSwitch as number],
async ([id, orderNoticeSwitch]: [number | undefined, number]) => {
await nextTick()
if (id && msgStore.unreadMsgLatest && orderNoticeSwitch === 1) {
const msgReturn = showOrderMsg(msgStore.unreadMsgLatest)
orderMsgReturn.value ||= msgReturn
} else {
Notification.remove(msgId.value)
}
},
{ immediate: true }
)
const handleBack = () => {
if (window.opener) {
window.close()
} else {
const routeTo = route.meta?.backBtn?.routeTo
if (routeTo) {
router.replace({
name: routeTo
})
}
}
}
onMounted(async () => {
msgStore.clearLoopTimer()
const [err, res] = await to(getSystemConfig())
if (!err && res) {
sysConfigStore.setUpdateConfig({
id: res.data.id,
orderNoticeSwitch: res.data.orderNoticeSwitch
})
doOrderLoop.value && msgStore.messageAndCountLoop()
}
})
onBeforeRouteLeave(() => {
Notification.remove(msgId.value)
msgStore.clearLoopTimer()
})
</script>
<style lang="scss" scoped>
@import "../assets/styles/mixin.scss";
$--layout-transition-all: all 0.2s cubic-bezier(0.34, 0.69, 0.1, 1);
$--layout-transition-margin: margin 0.2s cubic-bezier(0.34, 0.69, 0.1, 1);
$--layout-transition-left: left 0.2s cubic-bezier(0.34, 0.69, 0.1, 1);
.page-layout {
height: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
&--placeholer {
flex: none;
height: calc(var(--page-header-height) + var(--page-breadmenu-height));
transition: none;
&.no-nav {
height: var(--page-header-height);
}
}
&__header {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: var(--page-max-z-index);
flex: none;
padding: 0 16px;
height: var(--page-header-height);
border-bottom: var(--color-border-2) 1px solid;
background-color: #fff;
transition: $--layout-transition-all;
}
&__side {
position: fixed;
z-index: var(--page-max-z-index);
left: 0;
top: var(--page-header-height);
bottom: 0;
display: flex;
flex-direction: column;
:deep(.arco-menu-inner) {
flex: 1;
margin-bottom: var(--menu-collapse-width);
}
:deep(.arco-menu-collapse-button) {
position: absolute;
}
& + .page-layout__main {
margin-left: var(--menu-expand-width);
.page-layout__main-nav {
left: var(--menu-expand-width);
}
}
&.arco-menu-collapsed + .page-layout__main {
margin-left: var(--menu-collapse-width);
.page-layout__main-nav {
left: var(--menu-collapse-width);
}
}
}
&__main {
padding: var(--page-padding-gutter);
padding-top: 0;
transition: $--layout-transition-margin;
height: 100%;
display: flex;
flex-direction: column;
overflow-y: auto;
@include scrollbar(8px, transparent, var(--color-fill-4));
&.no-nav {
padding-top: var(--page-padding-gutter);
}
&-nav {
flex: none;
display: flex;
align-items: center;
background: var(--page-back-color);
position: fixed;
z-index: var(--page-max-z-index);
top: var(--page-header-height);
left: 0;
right: 0;
height: var(--page-breadmenu-height);
padding: 0 var(--page-padding-gutter);
transition: $--layout-transition-left;
}
&-content {
position: relative;
height: 100%;
display: flex;
flex-direction: column;
padding: var(--page-padding-gutter);
padding-top: --page-breadmenu-height;
background: #f9f9f9;
flex: 1;
// margin-left: var(--menu-expand-width);
// transition: margin-left 0.28s;
// transition: all 0.2s cubic-bezier(0.34, 0.69, 0.1, 1);
&.no-nav {
padding-top: var(--page-padding-gutter);
}
}
&-view {
flex: 1;
display: flex;
flex-direction: column;
background: #fff;
border-radius: 4px;
padding: var(--page-padding-gutter);
// > :last-child:not(.common-page-title) {
// flex: 1;
// display: flex;
// flex-direction: column;
// }
&-title {
display: flex;
justify-content: space-between;
align-items: center;
}
}
}
&.theme-dark {
.page-layout__header,
.page-layout__side {
background: var(--color-text-2);
}
}
}
</style>
<style lang="scss">
.order-msg__tip {
.arco-notification-title {
font-weight: 600;
}
.arco-notification-left {
padding-right: 5px;
.arco-notification-icon {
font-size: 16px;
color: var(--color-text-2);
}
}
.arco-notification-content {
margin-top: 6px;
}
}
</style>