Skip to content

Commit f2d9752

Browse files
authored
Merge pull request #98 from Tencent/feat/multiple-tabs-support
feat: 支持多标签tab页的功能
2 parents dda2f6e + f73de01 commit f2d9752

34 files changed

Lines changed: 406 additions & 65 deletions

File tree

.env

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
VITE_SOME_KEY=123
2-
# 打包路径
3-
VITE_BASE_URL = /
2+
# 打包路径 根据项目不同按需配置
3+
VITE_BASE_URL = ./

.eslintrc

Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,7 @@
1515
"defineProps": "readonly",
1616
"defineEmits": "readonly"
1717
},
18-
"plugins": [
19-
"vue",
20-
"@typescript-eslint"
21-
],
18+
"plugins": ["vue", "@typescript-eslint"],
2219
"parserOptions": {
2320
"parser": "@typescript-eslint/parser",
2421
"sourceType": "module",
@@ -28,27 +25,22 @@
2825
}
2926
},
3027
"settings": {
31-
"import/extensions": [
32-
".js",
33-
".jsx",
34-
".ts",
35-
".tsx"
36-
]
28+
"import/extensions": [".js", ".jsx", ".ts", ".tsx"]
3729
},
3830
"rules": {
3931
"no-console": "off",
4032
"no-continue": "off",
4133
"no-restricted-syntax": "off",
4234
"no-plusplus": "off",
43-
"no-param-reassign": "off",
44-
"no-shadow": "off",
35+
"no-param-reassign": "off",
36+
"no-shadow": "off",
4537
"guard-for-in": "off",
4638

4739
"import/extensions": "off",
4840
"import/no-unresolved": "off",
4941
"import/no-extraneous-dependencies": "off",
5042
"import/prefer-default-export": "off",
51-
43+
"import/first": "off", // https://github.com/vuejs/vue-eslint-parser/issues/58
5244
"@typescript-eslint/no-explicit-any": "off",
5345
"@typescript-eslint/explicit-module-boundary-types": "off",
5446
"vue/first-attribute-linebreak": 0
@@ -61,9 +53,8 @@
6153
"vue/require-default-prop": 0,
6254
"vue/multi-word-component-names": 0,
6355
"vue/no-reserved-props": 0,
64-
"vue/no-v-html": 0,
65-
56+
"vue/no-v-html": 0
6657
}
6758
}
6859
]
69-
}
60+
}

src/config/style.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export default {
88
isFooterAside: false,
99
isSidebarFixed: true,
1010
isHeaderFixed: true,
11+
isUseTabsRouter: false,
1112
showHeader: true,
1213
backgroundTheme: 'blueGrey',
1314
brandTheme: 'default',

src/interface.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { RouteRecordName } from 'vue-router';
12
import STYLE_CONFIG from '@/config/style';
23

34
export interface ResDataType {
@@ -37,3 +38,17 @@ export interface NotificationItem {
3738
date: string;
3839
quality: string;
3940
}
41+
42+
export interface TRouterInfo {
43+
path: string;
44+
routeIdx?: number;
45+
title?: string;
46+
name?: RouteRecordName;
47+
isAlive?: boolean;
48+
isHome?: boolean;
49+
}
50+
51+
export interface TTabRouterType {
52+
isRefreshing: boolean;
53+
tabRouterList: Array<TRouterInfo>;
54+
}

src/layouts/components/Content.vue

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,30 @@
11
<template>
2-
<router-view v-slot="{ Component }">
2+
<router-view v-if="!isRefreshing" v-slot="{ Component }">
33
<transition name="fade" mode="out-in">
4-
<component :is="Component" />
4+
<keep-alive :include="aliveViews">
5+
<component :is="Component" />
6+
</keep-alive>
57
</transition>
68
</router-view>
79
</template>
10+
11+
<script setup lang="ts">
12+
import { computed, ComputedRef } from 'vue';
13+
import { useTabsRouterStore } from '@/store';
14+
15+
const aliveViews = computed(() => {
16+
const tabsRouterStore = useTabsRouterStore();
17+
const { tabRouters } = tabsRouterStore;
18+
19+
return tabRouters.filter((route) => route.isAlive).map((route) => route.name);
20+
}) as ComputedRef<string[]>;
21+
22+
const isRefreshing = computed(() => {
23+
const tabsRouterStore = useTabsRouterStore();
24+
const { refreshing } = tabsRouterStore;
25+
return refreshing;
26+
});
27+
</script>
828
<style lang="less" scoped>
929
@import '@/style/variables';
1030

src/layouts/index.tsx

Lines changed: 122 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
1-
import { defineComponent, computed } from 'vue';
1+
import { defineComponent, computed, nextTick, onMounted, watch } from 'vue';
22
import { storeToRefs } from 'pinia';
3-
import { useRoute } from 'vue-router';
4-
import { usePermissionStore, useSettingStore } from '@/store';
3+
import { useRoute, useRouter } from 'vue-router';
4+
import { usePermissionStore, useSettingStore, useTabsRouterStore } from '@/store';
55

6-
import TDesignHeader from './components/Header.vue';
7-
import TDesignBreadcrumb from './components/Breadcrumb.vue';
8-
import TDesignFooter from './components/Footer.vue';
9-
import TDesignSideNav from './components/SideNav';
10-
import TDesignContent from './components/Content.vue';
6+
import LayoutHeader from './components/Header.vue';
7+
import LayoutBreadcrumb from './components/Breadcrumb.vue';
8+
import LayoutFooter from './components/Footer.vue';
9+
import LayoutSideNav from './components/SideNav';
10+
import LayoutContent from './components/Content.vue';
11+
import Setting from './setting.vue';
1112

1213
import { prefix } from '@/config/global';
13-
import TdesignSetting from './setting.vue';
14+
1415
import '@/style/layout.less';
1516

1617
const name = `${prefix}-base-layout`;
@@ -19,8 +20,10 @@ export default defineComponent({
1920
name,
2021
setup() {
2122
const route = useRoute();
23+
const router = useRouter();
2224
const permissionStore = usePermissionStore();
2325
const settingStore = useSettingStore();
26+
const tabsRouterStore = useTabsRouterStore();
2427
const { routers: menuRouters } = storeToRefs(permissionStore);
2528
const setting = storeToRefs(settingStore);
2629

@@ -57,10 +60,59 @@ export default defineComponent({
5760
return newMenuRouters;
5861
});
5962

63+
const appendNewRoute = () => {
64+
const {
65+
path,
66+
meta: { title },
67+
name,
68+
} = route;
69+
tabsRouterStore.appendTabRouterList({ path, title: title as string, name, isAlive: true });
70+
};
71+
72+
onMounted(() => {
73+
appendNewRoute();
74+
});
75+
76+
watch(
77+
() => route.path,
78+
() => {
79+
appendNewRoute();
80+
},
81+
);
82+
83+
const handleRemove = ({ value: path, index }) => {
84+
const { tabRouters } = tabsRouterStore;
85+
const nextRouter = tabRouters[index + 1] || tabRouters[index - 1];
86+
87+
tabsRouterStore.subtractCurrentTabRouter({ path, routeIdx: index });
88+
if (path === route.path) {
89+
router.push(nextRouter.path);
90+
}
91+
};
92+
const handleChangeCurrentTab = (path: string) => {
93+
router.push(path);
94+
};
95+
const handleRefresh = (currentPath: string, routeIdx: number) => {
96+
tabsRouterStore.toggleTabRouterAlive(routeIdx);
97+
nextTick(() => {
98+
tabsRouterStore.toggleTabRouterAlive(routeIdx);
99+
router.replace({ path: currentPath });
100+
});
101+
};
102+
const handleCloseAhead = (path: string, routeIdx: number) => {
103+
tabsRouterStore.subtractTabRouterAhead({ path, routeIdx });
104+
};
105+
const handleCloseBehind = (path: string, routeIdx: number) => {
106+
tabsRouterStore.subtractTabRouterBehind({ path, routeIdx });
107+
};
108+
const handleCloseOther = (path: string, routeIdx: number) => {
109+
tabsRouterStore.subtractTabRouterOther({ path, routeIdx });
110+
};
111+
60112
const renderSidebar = () => {
61113
return (
62114
settingStore.showSidebar && (
63-
<TDesignSideNav
115+
<LayoutSideNav
64116
showLogo={settingStore.showSidebarLogo}
65117
layout={settingStore.layout}
66118
isFixed={settingStore.isSidebarFixed}
@@ -75,7 +127,7 @@ export default defineComponent({
75127
const renderHeader = () => {
76128
return (
77129
settingStore.showHeader && (
78-
<TDesignHeader
130+
<LayoutHeader
79131
showLogo={settingStore.showHeaderLogo}
80132
theme={settingStore.displayMode}
81133
layout={settingStore.layout}
@@ -90,18 +142,72 @@ export default defineComponent({
90142
const renderFooter = () => {
91143
return (
92144
<t-footer class={`${prefix}-footer-layout`}>
93-
<TDesignFooter />
145+
<LayoutFooter />
94146
</t-footer>
95147
);
96148
};
97149

98150
const renderContent = () => {
99-
const { showBreadcrumb, showFooter } = settingStore;
151+
const { showBreadcrumb, showFooter, isUseTabsRouter } = settingStore;
152+
const { tabRouters } = tabsRouterStore;
100153
return (
101154
<t-layout class={[`${prefix}-layout`]}>
155+
{isUseTabsRouter && (
156+
<t-tabs
157+
theme="card"
158+
class={`${prefix}-layout-tabs-nav`}
159+
value={route.path}
160+
onChange={handleChangeCurrentTab}
161+
style={{ maxWidth: '100%', position: 'fixed', overflow: 'visible' }}
162+
onRemove={handleRemove}
163+
>
164+
{tabRouters.map((router: any, idx: number) => (
165+
<t-tab-panel
166+
value={router.path}
167+
key={`${router.path}_${idx}`}
168+
label={
169+
<t-dropdown
170+
trigger="context-menu"
171+
minColumnWidth={128}
172+
popupProps={{ overlayClassName: 'router-tabs-dropdown' }}
173+
v-slots={{
174+
dropdown: () => (
175+
<t-dropdown-menu>
176+
<t-dropdown-item onClick={() => handleRefresh(router.path, idx)}>
177+
<t-icon name="refresh" />
178+
刷新
179+
</t-dropdown-item>
180+
{idx > 0 && (
181+
<t-dropdown-item onClick={() => handleCloseAhead(router.path, idx)}>
182+
<t-icon name="arrow-left" />
183+
关闭左侧
184+
</t-dropdown-item>
185+
)}
186+
{idx < tabRouters.length - 1 && (
187+
<t-dropdown-item onClick={() => handleCloseBehind(router.path, idx)}>
188+
<t-icon name="arrow-right" />
189+
关闭右侧
190+
</t-dropdown-item>
191+
)}
192+
<t-dropdown-item onClick={() => handleCloseOther(router.path, idx)}>
193+
<t-icon name="close-circle" />
194+
关闭其它
195+
</t-dropdown-item>
196+
</t-dropdown-menu>
197+
),
198+
}}
199+
>
200+
{!router.isHome ? router.title : <t-icon name="home" />}
201+
</t-dropdown>
202+
}
203+
removable={!router.isHome}
204+
/>
205+
))}
206+
</t-tabs>
207+
)}
102208
<t-content class={`${prefix}-content-layout`}>
103-
{showBreadcrumb && <TDesignBreadcrumb />}
104-
<TDesignContent />
209+
{showBreadcrumb && <LayoutBreadcrumb />}
210+
<LayoutContent />
105211
</t-content>
106212
{showFooter && renderFooter()}
107213
</t-layout>
@@ -134,7 +240,7 @@ export default defineComponent({
134240
<t-layout class={this.mainLayoutCls}>{[sidebar, content]}</t-layout>
135241
</t-layout>
136242
)}
137-
<TdesignSetting />
243+
<Setting />
138244
</div>
139245
);
140246
},

src/layouts/setting.vue

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,9 @@
8282
<t-form-item label="显示 Footer" name="showFooter">
8383
<t-switch v-model="formData.showFooter" />
8484
</t-form-item>
85+
<t-form-item label="使用 多标签Tab页" name="isUseTabsRouter">
86+
<t-switch v-model="formData.isUseTabsRouter"></t-switch>
87+
</t-form-item>
8588
</t-form>
8689
<div class="setting-info">
8790
<p>请复制后手动修改配置文件: /src/config/style.ts</p>

src/pages/dashboard/base/index.vue

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,13 @@
202202
</div>
203203
</div>
204204
</template>
205+
206+
<script lang="ts">
207+
export default {
208+
name: 'DashboardBase',
209+
};
210+
</script>
211+
205212
<script setup lang="ts">
206213
import { onMounted, watch, ref, onUnmounted, nextTick, computed } from 'vue';
207214
@@ -386,9 +393,11 @@ const getRankClass = (index: number) => {
386393
return ['dashboard-rank', { 'dashboard-rank__top': index < 3 }];
387394
};
388395
</script>
396+
389397
<style lang="less" scoped>
390398
@import './index.less';
391399
</style>
400+
392401
<style lang="less">
393402
@import '@/style/variables.less';
394403

src/pages/dashboard/detail/index.vue

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,13 @@
5757
</card>
5858
</div>
5959
</template>
60+
61+
<script lang="ts">
62+
export default {
63+
name: 'DashboardDetail',
64+
};
65+
</script>
66+
6067
<script setup lang="ts">
6168
import { nextTick, onMounted, onUnmounted, watch, computed } from 'vue';
6269
@@ -149,6 +156,7 @@ const onMaterialChange = (value: string[]) => {
149156
lineChart.setOption(getFolderLineDataSet({ dateTime: value, ...chartColors.value }));
150157
};
151158
</script>
159+
152160
<style lang="less" scoped>
153161
@import url('./index.less');
154162
</style>

0 commit comments

Comments
 (0)