Skip to content

Commit

Permalink
feat: add tabs component
Browse files Browse the repository at this point in the history
  • Loading branch information
kirklin committed Jan 25, 2024
1 parent ebd4cb8 commit 72fd30e
Show file tree
Hide file tree
Showing 10 changed files with 255 additions and 1 deletion.
3 changes: 3 additions & 0 deletions apps/admin/autoResolver/auto-imports.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,7 @@ declare global {
const useStyleTag: typeof import('@vueuse/core')['useStyleTag']
const useSupported: typeof import('@vueuse/core')['useSupported']
const useSwipe: typeof import('@vueuse/core')['useSwipe']
const useTabs: typeof import('../src/composables/useTabs')['useTabs']
const useTemplateRefsList: typeof import('@vueuse/core')['useTemplateRefsList']
const useTextDirection: typeof import('@vueuse/core')['useTextDirection']
const useTextSelection: typeof import('@vueuse/core')['useTextSelection']
Expand Down Expand Up @@ -572,6 +573,7 @@ declare module 'vue' {
readonly useStyleTag: UnwrapRef<typeof import('@vueuse/core')['useStyleTag']>
readonly useSupported: UnwrapRef<typeof import('@vueuse/core')['useSupported']>
readonly useSwipe: UnwrapRef<typeof import('@vueuse/core')['useSwipe']>
readonly useTabs: UnwrapRef<typeof import('../src/composables/useTabs')['useTabs']>
readonly useTemplateRefsList: UnwrapRef<typeof import('@vueuse/core')['useTemplateRefsList']>
readonly useTextDirection: UnwrapRef<typeof import('@vueuse/core')['useTextDirection']>
readonly useTextSelection: UnwrapRef<typeof import('@vueuse/core')['useTextSelection']>
Expand Down Expand Up @@ -876,6 +878,7 @@ declare module '@vue/runtime-core' {
readonly useStyleTag: UnwrapRef<typeof import('@vueuse/core')['useStyleTag']>
readonly useSupported: UnwrapRef<typeof import('@vueuse/core')['useSupported']>
readonly useSwipe: UnwrapRef<typeof import('@vueuse/core')['useSwipe']>
readonly useTabs: UnwrapRef<typeof import('../src/composables/useTabs')['useTabs']>
readonly useTemplateRefsList: UnwrapRef<typeof import('@vueuse/core')['useTemplateRefsList']>
readonly useTextDirection: UnwrapRef<typeof import('@vueuse/core')['useTextDirection']>
readonly useTextSelection: UnwrapRef<typeof import('@vueuse/core')['useTextSelection']>
Expand Down
29 changes: 29 additions & 0 deletions apps/admin/src/composables/useTabs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import type { RouteLocationNormalized, Router } from "vue-router";

import { useRouter } from "vue-router";
import { unref } from "vue";
import type { Tab } from "~/store/modules/tabs";
import { useTabsStore } from "~/store/modules/tabs";

export function useTabs(_router?: Router) {
const tabStore = useTabsStore();
const router = _router || useRouter();

const { currentRoute } = router;

function getCurrentTab() {
const route = unref(currentRoute);
return tabStore.getTabsList.find(item => item.fullPath === route.fullPath)!;
}

return {
getTabsList: () => tabStore.getTabsList,
getPinnedTabsList: () => tabStore.getPinnedTabsList,
getLimitTabsList: () => tabStore.getLimitTabsList,
closeTab: (tab: Tab) => tabStore.closeTab(tab),
closePinnedTab: (tab: Tab) => tabStore.closePinnedTab(tab),
addTab: (route: RouteLocationNormalized) => tabStore.addTab(route),
pinnedTab: (tab: Tab) => tabStore.pinnedTab(tab),
getCurrentTab,
};
}
6 changes: 5 additions & 1 deletion apps/admin/src/layouts/header/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import UserInfoButton from "~/layouts/header/components/UserInfoButton.vue";
import SettingButton from "~/layouts/setting/index.vue";
import Breadcrumb from "~/layouts/header/components/Breadcrumb.vue";
import SearchAnyWhere from "~/layouts/header/components/SearchAnyWhere.vue";
import LayoutTabs from "~/layouts/tabs/index.vue";
defineOptions({
name: "HeaderLayout",
Expand All @@ -17,9 +18,12 @@ defineOptions({
<div class="md:block px-4">
<CollapseButton />
</div>
<div class="flex-grow-1">
<div class="flex-1">
<Breadcrumb />
</div>
<div class="flex flex-grow-1 items-center">
<LayoutTabs />
</div>
<div class="flex">
<NSpace>
<SearchAnyWhere />
Expand Down
125 changes: 125 additions & 0 deletions apps/admin/src/layouts/tabs/index.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
<script lang="ts" setup>
import { RouterTransitionConstants } from "@celeris/constants";
import { NTag } from "naive-ui";
import { useI18n } from "vue-i18n";
import { listenToRouteChange } from "~/router/mitt/routeChange";
import { useTabs } from "~/composables/useTabs";
defineOptions({
name: "LayoutTabs",
});
const router = useRouter();
const { addTab, pinnedTab, closePinnedTab, closeTab, getLimitTabsList, getPinnedTabsList, getCurrentTab } = useTabs();
function go(fullPath: string) {
router.push({ path: fullPath });
return true;
}
const { t, te } = useI18n();
function localize(key) {
return te(key) ? t(key) : key;
}
listenToRouteChange((route) => {
addTab(route);
});
</script>

<template>
<div class="flex layout-tags items-end">
<TransitionGroup :name="RouterTransitionConstants.FADE" tag="div" class="latest-list flex items-center gap-4">
<NTag
v-for="tab of getLimitTabsList()"
:key="tab.fullPath"
round
:bordered="false"
closable
@close="closeTab(tab)"
>
<span class="router-name" :class="{ 'current-tab': getCurrentTab()?.fullPath === tab.fullPath }" @click="go(tab.fullPath)">
{{ localize(tab.title) }}
</span>
<template #icon>
<div class="hover:color-primary-color hover:bg-[var(--action-color)] cursor-pointer mr-1 transition" @click="pinnedTab(tab)">
<CAIcon :size="14" icon="tabler:pinned" />
</div>
</template>
</NTag>
</TransitionGroup>

<div v-if="getLimitTabsList().length && getPinnedTabsList().length" class="divider-point h-1 w-1 relative top-2 z-1 rounded-full opacity-90 bg-primary-color mx-2" />
<TransitionGroup :name="RouterTransitionConstants.FADE" tag="div" class="pinned-list flex items-center gap-2">
<NTag
v-for="tab of getPinnedTabsList()"
:key="tab.name"
round
:bordered="false"
closable
@close="closePinnedTab(tab)"
>
<div class="router-name color-primary-color" :class="{ 'current-tab': getCurrentTab()?.fullPath === tab.fullPath }" @click="go(tab.fullPath)">
{{ localize(tab.title) }}
</div>
</NTag>
</TransitionGroup>
<div class="bar absolute bottom-[-0.5rem] bg-action-color rounded-2xl left-0 w-full h-1" />
</div>
</template>

<style scoped>
.layout-tags {
position: relative;
}
.layout-tags :deep(.ca-tag) {
background-color: transparent;
}
.layout-tags :deep(.ca-tag).ca-tag--round {
padding: 0;
transition-property: all;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 400ms;
}
.layout-tags :deep(.ca-tag) .ca-tag__icon {
margin: 0 !important;
}
.layout-tags :deep(.ca-tag) .ca-tag__close {
overflow: hidden;
width: 0;
margin-left: 0;
margin-right: 0;
transition-property: all;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 400ms;
}
.layout-tags :deep(.ca-tag):hover {
background-color: var(--action-color);
}
.layout-tags :deep(.ca-tag):hover.ca-tag--round {
padding: 0 calc(var(--n-height) / 3) 0 calc(var(--n-height) / 3);
}
.layout-tags :deep(.ca-tag):hover .ca-tag__close {
margin-left: 0.25rem;
overflow: unset;
width: 1rem;
}
.layout-tags :deep(.ca-tag) .ca-base-icon {
transform: unset;
}
.layout-tags .router-name {
cursor: pointer;
margin-left: 0.25rem;
margin-right: 0.25rem;
}
.layout-tags .router-name:hover {
text-decoration: underline;
text-decoration-thickness: 0.1rem;
text-decoration-color: var(--primary-color-hover);
}
.layout-tags .current-tab {
text-decoration: underline;
text-decoration-thickness: 0.1rem;
text-decoration-color: var(--primary-color);
}
</style>
3 changes: 3 additions & 0 deletions apps/admin/src/router/guard/stateGuard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { removeRouteChangeListener } from "~/router/mitt/routeChange";
import { useAppStore } from "~/store/modules/app";
import { usePermissionStore } from "~/store/modules/permission";
import { useUserStore } from "~/store/modules/user";
import { useTabsStore } from "~/store/modules/tabs";

export function createStateGuard(router: Router) {
router.afterEach((to) => {
Expand All @@ -12,9 +13,11 @@ export function createStateGuard(router: Router) {
const userStore = useUserStore();
const appStore = useAppStore();
const permissionStore = usePermissionStore();
const tabsStore = useTabsStore();
appStore.resetAPPState();
permissionStore.resetPermissionState();
userStore.resetUserState();
tabsStore.resetTabsState();
removeRouteChangeListener();
}
});
Expand Down
1 change: 1 addition & 0 deletions apps/admin/src/router/routes/modules/dashboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const dashboard: RouteRecordRaw = {
title: "routes.dashboard.dashboard",
icon: "i-mdi-monitor-dashboard",
shouldHideInMenu: true,
shouldAffixToNavBar: true,
},
},
],
Expand Down
84 changes: 84 additions & 0 deletions apps/admin/src/store/modules/tabs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { defineStore } from "pinia";
import { APP_TABS_STORE_ID, PageConstants } from "@celeris/constants";
import type { RouteLocationNormalized, RouteRecordName } from "vue-router";
import { takeRight, uniqBy } from "@celeris/utils";
import { PAGE_NOT_FOUND_ROUTE, REDIRECT_ROUTE } from "~/router/routes/basic";

interface AppTabsState {
tabs: Tab[];
pinnedTabs: Tab[];
maxVisibleTabs: number;
}
export interface Tab {
name: RouteRecordName;
fullPath: string;
title: string;
}
export const useTabsStore = defineStore({
id: APP_TABS_STORE_ID,
persist: [{
paths: ["pinnedTabs"],
storage: localStorage,
}, {
paths: ["tabs"],
storage: sessionStorage,
}],
state: (): AppTabsState => ({
tabs: [],
pinnedTabs: [],
maxVisibleTabs: 5,
}),
getters: {
getTabsList(state): Tab[] {
return state.tabs;
},
getLimitTabsList(state): Tab[] {
return takeRight(
state.tabs.filter(tab => state.pinnedTabs.findIndex(p => p.fullPath === tab.fullPath) === -1).reverse(),
this.maxVisibleTabs,
);
},
getPinnedTabsList(state): Tab[] {
return state.pinnedTabs;
},
},
actions: {
addTab(route: RouteLocationNormalized) {
const { path, name, meta } = route;
if (!name || path === PageConstants.ERROR_PAGE || path === PageConstants.BASE_LOGIN || [REDIRECT_ROUTE.name, PAGE_NOT_FOUND_ROUTE.name].includes(name)) {
return;
}
const title = meta?.title as string || name.toString().split("-").at(-1);
if (title) {
const newTab: Tab = { name, fullPath: route.fullPath, title };
this.tabs = uniqBy([newTab, ...this.tabs], "fullPath");
}
},
close(isPinned: boolean, tab: Tab) {
const targetTabs = isPinned ? this.pinnedTabs : this.tabs;
this.tabs = targetTabs.filter(currentTab => currentTab.fullPath !== tab.fullPath);
},
closeTab(tab: Tab) {
this.close(false, tab);
},
closePinnedTab(tab: Tab) {
this.close(true, tab);
},
pinnedTab(tab: Tab) {
const isPresent = this.pinnedTabs.some(pinnedTab => pinnedTab.fullPath === tab.fullPath);
if (!isPresent) {
this.pinnedTabs = [tab, ...this.pinnedTabs];
}
return true;
},
resetTabsState() {
this.tabs = [];
this.pinnedTabs = [];
},
},
});

// Need to be used outside the setup
export function useTabsStoreWithOut() {
return useTabsStore(store);
}
1 change: 1 addition & 0 deletions packages/node/vite/src/plugins/unocss.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ export function createUnoCSSPluginConfig(): PluginOption {
primary_8: "var(--primary-color-8)",
primary_9: "var(--primary-color-9)",
primary_10: "var(--primary-color10)",
action: "var(--action-color)",
info: "var(--info-color)",
info_hover: "var(--info-color-hover)",
info_suppl: "var(--info-color-suppl)",
Expand Down
2 changes: 2 additions & 0 deletions packages/web/constants/src/storageConstants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ export const LOCALES_STORE_KEY = "LOCALES__STORE__KEY";

export const APP_STORE_ID = "APP_STORE";

export const APP_TABS_STORE_ID = "APP_TABS_STORE";

export const APP_PERMISSION_STORE_ID = "APP_PERMISSION_STORE";

export const APP_USER_STORE_ID = "APP_USER_STORE";
Expand Down
2 changes: 2 additions & 0 deletions packages/web/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,6 @@ export {
intersection,
uniqBy,
pick,
split,
takeRight,
} from "lodash-es";

2 comments on commit 72fd30e

@vercel
Copy link

@vercel vercel bot commented on 72fd30e Jan 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

celeris-web-api – ./services/admin

celeris-web-api-git-master-kirklin.vercel.app
celeris-web-api.vercel.app
celeris-web-api-kirklin.vercel.app

@vercel
Copy link

@vercel vercel bot commented on 72fd30e Jan 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

celeris-web – ./apps/admin

celeris-web-kirklin.vercel.app
celeris-web.vercel.app
celeris-web-git-master-kirklin.vercel.app

Please sign in to comment.