<template> <client-only> <aside class="nav-sidebar" :class="[ { ' js-sidebar-collapsed ': isSidebarCollapsed, ' sidebar-expanded-mobile ': isMobile && !isSidebarCollapsed, ' sidebar-collapsed-desktop ': isDesktop && isSidebarCollapsed, }, buildName() + '-sidebar', ]" > <!-- #region mobile-header --> <div class="mobile-header pt-2 p-0"> <NuxtLink to="/" classes="btn mobile-close-sidebar"> <img :src="logo" :alt="appLongTitle()" class="img-fluid" style="width: 2em; filter: invert(0)" /> {{ appShortTitle() }} </NuxtLink> <button-component @click="sidebarCollapsedSetter(true)" classes="mobile-close-sidebar" buttonText="" > <span class="tavasi tavasi-Component-71--1"></span> </button-component> </div> <!-- #endregion mobile-header --> <!-- #region sidebar --> <div class="nav-sidebar-inner-scroll d-flex flex-column firefox-scrollbar justify-content-between" > <ClientOnly> <ul class="sidebar-top-level-items"> <template v-for="(menuItem, mainKey) in menu"> <li v-if="isShowMenuItem(mainKey, menuItem)" class="mb-1" @mouseenter="emitCustomEvent($event, 'in')" @mouseleave="emitCustomEvent($event, 'out')" v-for="(item, key, index) in menuItem" :key="index" :class="['color-' + item.color]" > <template v-if="isShowMenuItem(key, item)"> <template v-if="item?.subMenu?.length"> <NuxtLink :to="{ name: item.link }" class="has-sub-items gl-link" :class="{ active: $route.name.startsWith(item.link) }" > <!-- <i class="nav-icon-container" :class="item.icon"></i> --> <svg class="nav-icon-container" :class="'icon icon-' + item.icon" > <use :xlink:href="'#icon-' + item.icon"></use> </svg> <span class="nav-item-name"> {{ $t(translateKey(item)) }} </span> </NuxtLink> <ul class="sidebar-sub-level-items li-child" :id="item.link" > <li class="fly-out-top-item active gl-link"> <span class="fly-out-top-item-container"> <strong class="fly-out-top-item-name"> {{ $t(translateKey(item)) }} </strong> </span> </li> <li class="divider fly-out-top-item"></li> <li class="li-child" v-for="(child, j) in item.subMenu" :key="j" > <template v-if="child?.subMenu?.length"> <NuxtLink :to="{ name: child.link }" class="has-sub-items gl-link" exact-path > <!-- <i class="nav-icon-container" :class="child.icon"></i> --> <svg class="nav-icon-container" :class="'icon icon-' + child.icon" > <use :xlink:href="'#icon-' + child.icon"></use> </svg> <span class="nav-item-name"> {{ $t(translateKey(child)) }} </span> </NuxtLink> <ul class="sidebar-sub-level-items" :id="child.link"> <li class="fly-out-top-item active gl-link"> <span class="fly-out-top-item-container"> <strong class="fly-out-top-item-name"> {{ $t(translateKey(child)) }} </strong> </span> </li> <li class="divider fly-out-top-item"></li> <li v-for="(descendant, k) in child.subMenu" @click.prevent="navigateTo(descendant)" :key="k" > <NuxtLink exact-path :to="{ name: descendant.link }" :title="descendant.title" class="gl-link btn sub-level-item" > <!-- <svg-icon-component :iconName="descendant.icon" /> --> <svg class="nav-icon-container" :class="'icon icon-' + descendant.icon" > <use :xlink:href="'#icon-' + descendant.icon" ></use> </svg> <!-- <i class="nav-icon-container " :class="descendant.icon" > <span class="path1"></span ><span class="path2"></span> </i> --> <span class="nav-item-name gl-link me-1"> {{ descendant.title }} </span> </NuxtLink> </li> </ul> </template> <NuxtLink v-else exact-path :to="{ name: child.link }" :title="$t(translateKey(child))" class="gl-link btn sub-level-item" > <svg-icon-component :iconName="child.icon" /> <svg class="nav-icon-container" :class="'icon icon-' + child.icon" > <use :xlink:href="'#icon-' + child.icon"></use> </svg> <!-- <i class="nav-icon-container " :class="child.icon"> <span class="path1"></span><span class="path2"></span> </i> --> <span class="nav-item-name gl-link me-1"> {{ $t(translateKey(child)) }} </span> </NuxtLink> </li> </ul> </template> <NuxtLink v-else :to="{ name: item.link }" :title="$t(translateKey(item))" class="gl-link" exact-path > <!-- <svg-icon-component :iconName="item.icon" /> --> <svg class="nav-icon-container" :class="'icon icon-' + item.icon" > <use :xlink:href="'#icon-' + item.icon"></use> </svg> <!-- <i class="nav-icon-container" :class="item.icon"> <span class="path1"></span><span class="path2"></span ></i> --> <span class="nav-item-name"> {{ $t(translateKey(item)) }} </span> </NuxtLink> </template> </li> </template> </ul> <!-- <template v-if="isSidebarCollapsed"> --> <div class="mb-5 mb-md-0"> <ul class="sidebar-top-level-items m-0"> <li v-if="!isMajlesBuild()" class="" @mouseenter="emitCustomEvent($event, 'in')" @mouseleave="emitCustomEvent($event, 'out')" > <a class="has-sub-items gl-link"> <svg v-if="isGuest && showRegister" class="icon icon-personal" > <use class="icon-personal" xlink:href="#icon-personal" ></use> </svg> <img v-else class="nav-icon-container img-fluid user-avatar" :src="avatar" /> <span class="nav-item-name"> {{ currentUser?.user_data?.first_name }} </span> </a> <ul class="sidebar-sub-level-items"> <!-- <li class="fly-out-top-item gl-link"> <h6 class="bg-white text-center py-3"> {{ userFullname(currentUser?.user_data) }} </h6> </li> --> <li class="fly-out-top-item gl-link"> <NuxtLink v-can="'admin_dahsboard'" :to="{ name: 'admin' }" class="dropdown-item" exact-path >{{ $t("admin") }}</NuxtLink > </li> <li class="fly-out-top-item gl-link"> <a href="install PWA" title="نصب سامانه - در صفحه گوشی یا رایانه" class="dropdown-item" type="button" @click.prevent="checkBeforeInstallingPwa()" > {{ $t("InstallSystem") }} </a> </li> <li class="fly-out-top-item gl-link"> <a href="exit" @click.prevent="logout()" class="dropdown-item" > {{ $t("Exit") }} </a> </li> <li class="fly-out-top-item gl-link"> <a href="/refresh-page" v-if="isNewVersionAvailable" @click="reloadPage()" class="dropdown-item" > <svg style="height: 1em" class="icon icon-reset-form"> <use xlink:href="#icon-reset-form"></use> </svg> بروزرسانی </a> </li> <li class="fly-out-top-item gl-link"> <a v-if="isGuest && showRegister" title="ثبت نام" href="/login" class="dropdown-item" >ثبت نام </a> </li> </ul> </li> <template> <li class=""> <a @click.prevent="sidebarCollapsedSetter(!isSidebarCollapsed)" class="toggle-sidebar-button" role="button" title="بازکردن" type="button" > <svg class="icon icon-chevron-double-left"> <use xlink:href="#icon-chevron-double-left"></use> </svg> <!-- <svg class="icon icon-chevron-double-left" data-testid="chevron-double-lg-left-icon" > <use href="@assets/common/img/icons.svg#chevron-double-lg-left" ></use> </svg> --> <span class="collapse-text me-2">{{ $t("Close") }}</span> </a> </li> </template> </ul> </div> </ClientOnly> </div> <!-- #endregion sidebar --> <!-- #region base-modal --> <base-modal-v2 v-if="uploadForFirstTime" @close="closeModal" @canel="closeModal()" :showHeaderCloseButton="true" :modalTitle="modalTitle" class="borhan-modal" modalSize="modal-lg" height="30em" overflow="hidden" :showSaveButton="false" :showCloseButton="false" > <settings :uploadForFirstTime="uploadForFirstTime" :menuId="menuId" @canel="closeModal()" @close="closeModal()" @after-select-file="afterSelectFile()" ></settings> </base-modal-v2> <!-- #endregion base-modal --> <SvgIconComponent></SvgIconComponent> </aside> </client-only> </template> <script> import { mapState, mapActions } from "pinia"; import { useCommonStore } from "~/stores/commonStore"; import { useStorage } from "@vueuse/core"; // import Settings from "./dashboard/default/Settings.vue"; /** * @vue-prop {object} [menu={}] - منوی سایدبار که در قسمت والد سایدبار پر میگردد * @vue-prop {Boolean} [showUserAvatar=false] - نمایش آواتار کاربر * * @vue-data {Number} [top=0] - موقعیت بالا * @vue-data {Number} [menuColor=1] - رنگ منو * @vue-data {String|null} [slotComponentName=null] - نام کامپوننت اسلات * @vue-data {String|null} [modalTitle=null] - عنوان مودال * @vue-data {Boolean} [uploadForFirstTime=false] - آپلود برای اولین بار * @vue-data {Number} [statuspag=1] - وضعیت صفحه * @vue-data {Array} [topico=[]] - آیتمهای بالا * @vue-data {Array} [botico=[]] - آیتمهای پایین * * @vue-computed {Boolean} [isSidebarCollapsed] - وضعیت فشرده شدن سایدبار * @vue-computed {Boolean} [isDesktop] - وضعیت دسکتاپ بودن * @vue-computed {Boolean} [isMobile] - وضعیت موبایل بودن * * @vue-event {Function} mapActions.sidebarCollapsedSetter - تغییر وضعیت فشرده شدن یا عدم فشرده شدن سایدبار. */ export default { props: { menu: { default() { return []; }, }, showUserAvatar: { default: false, }, showRegister: { default: true, }, }, async beforeMount() { const res = await userAvatar(this.currentUser?.user_data); this.avatar = res.default; const logo = await logoPhoto(); this.logo = logo.default; }, mounted() { // logoPhoto().then((img) => { // this.logo = img; // }); this.envVersion = import.meta.env.VITE_VERSION; this.lsVersion = useStorage("app-version", "0.0.0").value; if (this.lsVersion) { if (this.envVersion != this.lsVersion) this.isNewVersionAvailable = true; } const { $eventBus } = useNuxtApp(); $eventBus.on("show-settings-modal", (activeTabId) => { console.info(activeTabId); this.openModal("Settings", "تنظیمات", activeTabId); }); }, data() { return { isNewVersionAvailable: false, envVersion: null, lsVersion: null, top: 0, menuId: 1, menuColor: 1, slotComponentName: null, modalTitle: null, uploadForFirstTime: false, //بلا استفاده ها statuspag: 1, topico: [], botico: [], logo: "", avatar: "", }; }, computed: { ...mapState(useCommonStore, [ "isSidebarCollapsed", "isDesktop", "isMobile", "currentUser", "isGuest", ]), }, methods: { ...mapActions(useCommonStore, ["sidebarCollapsedSetter"]), isShowMenuItem(key, menuItem) { let res = menuItem.show ?? true; if (res && isMajlesBuild()) { if (key == "LawDraft" || key == "notifications" || key == "favorite") res = false; // else if ( // menuItem?.link == "searchNavigation" || // menuItem?.link == "searchChart" // ) // res = false; } return res; }, /** * صفحه فعلی را مجدداً بارگذاری میکند. */ reloadPage() { location.reload(); }, /** * قبل از تلاش برای نصب یک اپلیکیشن وب پیشرونده (PWA) مرورگر را بررسی میکند. * اگر مرورگر Firefox باشد، به کاربر اعلام میکند که PWA پشتیبانی نمیشود. * در غیر این صورت، فرآیند نصب PWA را ادامه میدهد. */ checkBeforeInstallingPwa() { if (identifyBrowser().includes("Firefox")) { this.alertPwaNotSupported(); } else { this.installPwa(); } }, /** * این متد رنگ منو را با توجه به رنگ مسیر آن تنظیم میکند. * @param {Object} route - مسیر برای ناوبری * @param {String} route.color - رنگ مسیر * @param {String} route.link - لینک مسیر */ navigateTo(route) { this.menuColor = route.color; this.$router.push({ name: route.link }).catch(() => {}); }, /** * این متد ترجمه ها را چک میکند. اگر ترجمه وجود نداشته باشد، عنوان آیتم برگردانده میشود. * @param {Object} item - آیتم برای ترجمه * @param {String} item.translateKey - کلید ترجمه * @param {String} item.title - عنوان آیتم * @returns {String} - کلید ترجمه یا عنوان آیتم */ translateKey(item) { if (item.translateKey) return item.translateKey; else return item.title; }, /** * دکمه تغییر ظاهری سایدبار */ toggleSidebarMenu() { this.$store.commit("TOGGLE_SIDEBAR_MENU"); }, /** * این متد یک رویداد سفارشی را با محاسبه موقعیت Y صفحه نمایش میدهد. * @param {Event} event - رویداد سفارشی * @param {String} type - نوع رویداد */ emitCustomEvent(event, type) { const posY = event.pageY - event.offsetY; if (event.target?.lastChild?.style) event.target.lastChild.style.top = posY + "px"; this.showfilter(); }, /** * این متد پنل فیلتر را نمایش میدهد. */ showfilter() { // this.$emit("statusPag", this.statuspag = 1); }, /** * این متد یک مودال را باز میکند و مقادیر مربوط به نام کامپوننت اسلات و عنوان مودال را تنظیم میکند. * @param {String} componentName - نام کامپوننت اسلات * @param {String} title - عنوان مودال */ openModal(componentName, title, menuId) { this.uploadForFirstTime = true; this.slotComponentName = componentName; this.modalTitle = title; this.menuId = menuId; // setTimeout(() => { // $("#base-modal").modal({ backdrop: "static", keyboard: false }, "show"); // }, 500); }, /** * این متد مودال را میبندد و مقادیر مربوط به آپلود را ریست میکند. */ closeModal() { // $("#base-modal").modal("hide"); // setTimeout(() => { this.uploadForFirstTime = false; // this.createOntology(); // }, 500); }, }, components: { Settings: defineAsyncComponent(() => import("@/components/dashboard/default/Settings.vue") ), SvgIconComponent: defineAsyncComponent(() => import("@/components/other/SvgIconComponent.vue") ), }, }; </script> <style lang="scss"> .square.s16, svg.s16 { width: 16px; height: 16px; } svg.s12 { width: 12px; height: 12px; } svg.s18 { width: 18px; height: 18px; } </style> <style scoped lang="scss"> // .nav-sidebar { position: fixed; bottom: 0; right: 0; transition: width 0.2s, left 0.2s; z-index: 1041; width: var(--sidebar-width); top: var(--header-height, 48px); // background-color: var(--the-sidebar-background-color); border-left: 1px solid #e9e9e9; &.majles-sidebar { border: unset; &.collapse-text { text-decoration: none; display: flex; } .nav-item-name { display: block; } svg.s12 { width: 1em; height: 1em; } &.sidebar-collapsed-desktop { width: var(--sidebar-collapsed-width); } .nav-sidebar-inner-scroll { height: 100%; width: 100%; overflow-x: hidden; overflow-y: auto; margin-top: unset; } .dropdown-item { color: #fff; } } &.sidebar-expanded-mobile { right: 0 !important; } li.active > a, li:hover > a { // background-color: #d8f8fd; // background-color: rgb(177 244 255); background-color: var(--active-sidebar); color: var(--sidebar-active-text-color) !important; // margin-right: 1em; svg { color: var(--sidebar-active-text-color) !important; } } .li-child { .sub-level-item { height: 3em; font-size: 0.8rem !important; } .NuxtLink-active { background-color: var(--sidebar-child-background-color) !important; } .li-child.active > a, .li-child:hover > a { // background-color: #d8f8fd; // background-color: rgb(177 244 255); background-color: var(--sidebar-child-background-color); color: var(--sidebar-active-text-color) !important; // margin-right: 1em; svg { color: var(--sidebar-active-text-color) !important; } } } li.active:not(.fly-out-top-item) > a:not(.has-sub-items) { background-color: rgba(121, 248, 222, 0.301); } li > a, li > .fly-out-top-item-container { margin: 0; // margin-right: 1em; height: 47px; border-top-right-radius: 20px; border-bottom-right-radius: 20px; transition: 250ms; padding-right: 1em; // padding: 3px 7px; display: flex; align-items: center; // border-radius: 0.25rem; width: auto; line-height: 3rem; transition: none; // margin: 1px 8px; text-decoration: none; justify-content: center; } .nav-sidebar-inner-scroll { height: 100%; width: 100%; overflow-x: hidden; overflow-y: auto; // margin-top: 0.25rem; background: var(--linear-gradient-sidebar); .has-sub-items.NuxtLink-active { // background-color: #fbfafd !important; background-color: var(--active-sidebar); font-weight: bold; // & + .sidebar-sub-level-items { // display: none; // } } .has-sub-items.NuxtLink-active + .sidebar-sub-level-items { opacity: 1; display: block; position: static; box-shadow: none; border-style: none; visibility: hidden; visibility: visible; padding-right: 1em; & > .fly-out-top-item { display: none; } .has-sub-items.NuxtLink-active { background-color: var(--active-sidebar); // background-color: #fbfafd !important; font-weight: bold; // & + .sidebar-sub-level-items { // display: none; // } & + .sidebar-sub-level-items { opacity: 1; display: block; position: static; box-shadow: none; border-style: none; visibility: hidden; visibility: visible; padding-right: 0.4em; .fly-out-top-item { display: none; } } } } .sidebar-top-level-items { margin-bottom: 60px; li { white-space: nowrap; position: relative; & > .gl-link { font-size: 0.875rem; // color: #76a5cc; color: var(--sidebar-text-color); .tavasi { font-size: 1.1rem; } & > .icon { width: var(--sidebar-icons-width); height: var(--sidebar-icons-height); } } .context-header { position: relative; margin-left: 2px; // width: 256px; width: var(--sidebar-width); } &:hover > .sidebar-sub-level-items { visibility: visible; opacity: 1; transition: all 300ms ease-out; // transform: translate(0em, -2em); .nav-item-name { display: inline-block; } } .nav-icon-container { display: flex; margin-left: 0px; color: var(--sidebar-text-color); } @media (min-width: 576px) { a.has-sub-items + .sidebar-sub-level-items { min-width: 10.5em; margin-top: 0.2em; } } .sidebar-sub-level-items { list-style: none; position: fixed; // margin-right: 0.5rem; margin-right: 0; margin-top: 0; padding-left: 0; padding-right: 0; padding-bottom: 0.25rem; padding-top: 0; background-color: var(--the-sidebar-background-color); box-shadow: 0 0.25rem 1rem rgba(31, 30, 36, 0.24), 0 0 0.125rem rgba(31, 30, 36, 0.24); border-style: none; border-radius: 4px; visibility: hidden; opacity: 0; // right: 0; // right: 3.5em; // right:80px; right: calc(var(--sidebar-width) - 1px); li { margin-bottom: 0.2em; &:not(.fly-out-top-item) { padding: 0 0.4em; } // &:hover { // cursor: pointer; // } a:hover > .has-sub-items { & + .sidebar-sub-level-items { // right: calc(var(--sidebar-collapsed-width) + 11em - 1px); transform: translate(-0.3em, -2em); // visibility: visible; } &.NuxtLink-exact-active.NuxtLink-active + .sidebar-sub-level-items { // right: unset; transform: unset; // visibility: visible; } } .fly-out-top-item-container { margin: 0; border-radius: 0; border-bottom: 1px solid var(--sidebar-text-color); font-size: 1rem; padding: 0.5em; } } .sub-level-item { display: flex; align-items: center; justify-content: center; margin: auto; border-radius: 0.5rem; width: 100%; opacity: 1 !important; } } } // .nav-icon-container { // margin-left: 8px; // } .badge.badge-pill:not(.fly-out-badge), .nav-item-name { // border: 0; // clip: rect(0, 0, 0, 0); // height: 1px; // margin: -1px; // overflow: hidden; // padding: 0; // position: absolute; // white-space: nowrap; // width: 1px; text-align: right; text-align: start; flex: 1; overflow: hidden; text-overflow: ellipsis; margin-right: 3px; } li:not(.fly-out-top-item) > .NuxtLink-active { // background-color: #bdf6ff63; background-color: var(--active-sidebar); color: #374151 !important; font-family: sahel-semi-bold; * { color: #374151; fill: #374151; } } li:not(.fly-out-top-item):hover > .sub-level-item { // background-color: rgba(19, 81, 80, 0.49); } } } &.js-sidebar-collapsed { width: var(--sidebar-collapsed-width); .nav-sidebar-inner-scroll { .sidebar-top-level-items { li { .context-header { } & > a, & > .fly-out-top-item-container { // margin-right: 0; padding-right: 0; } &:hover > .sidebar-sub-level-items { visibility: visible; opacity: 1; transition: all 300ms ease-out; } &.fly-out-top-item { display: block; } .nav-icon-container { margin-right: -1em; } @media (min-width: 576px) { a.has-sub-items + .sidebar-sub-level-items { } } .sidebar-sub-level-items { position: fixed; padding-right: 0; box-shadow: 0 0.25rem 1rem rgba(31, 30, 36, 0.24), 0 0 0.125rem rgba(31, 30, 36, 0.24); border-style: none; visibility: hidden; opacity: 0; right: var(--sidebar-collapsed-width); li { position: relative; &:not(.fly-out-top-item) { } &:hover > .has-sub-items { & + .sidebar-sub-level-items { right: calc(var(--sidebar-collapsed-width) + 10.2em - 1px); transform: translate(-1.7em, -2em); visibility: visible; } &.NuxtLink-exact-active.NuxtLink-active + .sidebar-sub-level-items { right: unset; transform: unset; visibility: visible; } } .fly-out-top-item-container { } .sidebar-sub-level-items { // position: absolute!important; // top: 0; // right: 100%; visibility: hidden; } } .sub-level-item { } } } // .nav-icon-container { // } .badge.badge-pill:not(.fly-out-badge), .nav-item-name { display: none; } li:not(.fly-out-top-item) > .NuxtLink-active { // background-color: #bdf6ff63; background-color: var(--active-sidebar); margin-right: 1em; // .nav-icon-container { // margin-right: -1em; // } } } } .has-sub-items.NuxtLink-active + .sidebar-sub-level-items { .fly-out-top-item { display: block; } } .toggle-sidebar-button { // width: calc(var(--sidebar-collapsed-width) - 1px); // padding: 0 21px; justify-content: center; .icon-chevron-double-left { transform: rotateY(0deg); } .collapse-text { text-decoration: none; display: none; } } } &.sidebar-collapsed-desktop { width: var(--sidebar-collapsed-width); .nav-sidebar-inner-scroll { .sidebar-top-level-items { .sidebar-sub-level-items { right: 80px; // right: var(--sidebar-collapsed-width); } // .nav-icon-container { // margin-left: 0 !important; // } } .toggle-sidebar-button { width: calc(var(--sidebar-collapsed-width) - 1px); padding: 0 21px; .collapse-text { text-decoration: none; display: none; } } } } &.sidebar-expanded-mobile { right: 0 !important; } .toggle-sidebar-button { border: 0; padding: 0 1em; transition: width 0.2s; color: var(--sidebar-text-color); display: flex; align-items: center; justify-content: center; // position: fixed; bottom: 0; width: var(--sidebar-width); height: 3em; &:hover { background-color: var(--active-sidebar); color: var(--sidebar-active-text-color) !important; svg { color: var(--sidebar-active-text-color) !important; } } .icon-chevron-double-left { width: 1.3em; height: 100%; transform: rotateY(-180deg); } } } .mobile-header { display: none; } .text-truncate { display: inline-block; max-width: 6em; /* میتوانید این مقدار را با توجه به نیازتان تنظیم کنید */ white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: var(--sidebar-text-color) !important; &:hover { color: inherit !important; } } .nav-link-bottem { width: 10em; } .icon-personal { margin-left: 1em; } </style>