mirror of
https://github.com/Tencent/tdesign-vue-next-starter.git
synced 2024-11-10 07:28:24 +08:00
feat: 打开外部页面(内嵌/外链) (#377)
This commit is contained in:
parent
6b1e856657
commit
05ded03b50
34
src/hooks/event/useWindowSizeFn.ts
Normal file
34
src/hooks/event/useWindowSizeFn.ts
Normal file
|
@ -0,0 +1,34 @@
|
|||
import debounce from 'lodash/debounce';
|
||||
import { onUnmounted, onMounted } from 'vue';
|
||||
|
||||
interface WindowSizeOptions {
|
||||
immediate?: boolean;
|
||||
}
|
||||
|
||||
interface Fn<T = any, R = T> {
|
||||
(...arg: T[]): R;
|
||||
}
|
||||
|
||||
export function useWindowSizeFn<T>(fn: Fn<T>, options?: WindowSizeOptions, wait = 150) {
|
||||
const handleSize: () => void = debounce(fn, wait);
|
||||
|
||||
const start = () => {
|
||||
if (options && options.immediate) {
|
||||
fn();
|
||||
}
|
||||
window.addEventListener('resize', handleSize);
|
||||
};
|
||||
|
||||
const stop = () => {
|
||||
window.removeEventListener('resize', handleSize);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
start();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
stop();
|
||||
});
|
||||
return [start, stop];
|
||||
}
|
|
@ -6,12 +6,14 @@
|
|||
</keep-alive>
|
||||
</transition>
|
||||
</router-view>
|
||||
<frame-page />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import type { ComputedRef } from 'vue';
|
||||
import { useTabsRouterStore } from '@/store';
|
||||
import FramePage from '@/layouts/frame/index.vue';
|
||||
|
||||
// <suspense>标签属于实验性功能,请谨慎使用
|
||||
// 如果存在需解决/page/1=> /page/2 刷新数据问题 请修改代码 使用activeRouteFullPath 作为key
|
||||
|
|
10
src/layouts/components/FrameBlank.vue
Normal file
10
src/layouts/components/FrameBlank.vue
Normal file
|
@ -0,0 +1,10 @@
|
|||
<template>
|
||||
<div></div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'FrameBlank',
|
||||
});
|
||||
</script>
|
99
src/layouts/components/FrameContent.vue
Normal file
99
src/layouts/components/FrameContent.vue
Normal file
|
@ -0,0 +1,99 @@
|
|||
<template>
|
||||
<div :class="prefixCls" :style="getWrapStyle">
|
||||
<t-loading :loading="loading" size="large" :style="getWrapStyle">
|
||||
<iframe ref="frameRef" :src="frameSrc" :class="`${prefixCls}__main`" @load="hideLoading"></iframe>
|
||||
</t-loading>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { CSSProperties, watch, ref, unref, computed } from 'vue';
|
||||
import debounce from 'lodash/debounce';
|
||||
import { useWindowSizeFn } from '@/hooks/event/useWindowSizeFn';
|
||||
import { prefix } from '@/config/global';
|
||||
import { useSettingStore } from '@/store';
|
||||
|
||||
defineProps({
|
||||
frameSrc: String,
|
||||
});
|
||||
|
||||
const loading = ref(true);
|
||||
const heightRef = ref(window.innerHeight);
|
||||
const frameRef = ref<HTMLFrameElement>();
|
||||
const prefixCls = computed(() => [`${prefix}-iframe-page`]);
|
||||
const settingStore = useSettingStore();
|
||||
|
||||
const getWrapStyle = computed((): CSSProperties => {
|
||||
return {
|
||||
height: `${unref(heightRef)}px`,
|
||||
};
|
||||
});
|
||||
|
||||
const computedStyle = getComputedStyle(document.documentElement);
|
||||
const sizeXxxl = computedStyle.getPropertyValue('--td-comp-size-xxxl');
|
||||
const paddingTBXxl = computedStyle.getPropertyValue('--td-comp-paddingTB-xxl');
|
||||
|
||||
function getOuterHeight(dom: Element) {
|
||||
let height = dom.clientHeight;
|
||||
const computedStyle = window.getComputedStyle(dom);
|
||||
height += parseInt(computedStyle.marginTop, 10);
|
||||
height += parseInt(computedStyle.marginBottom, 10);
|
||||
height += parseInt(computedStyle.borderTopWidth, 10);
|
||||
height += parseInt(computedStyle.borderBottomWidth, 10);
|
||||
return height;
|
||||
}
|
||||
|
||||
function calcHeight() {
|
||||
const iframe = unref(frameRef);
|
||||
if (!iframe) {
|
||||
return;
|
||||
}
|
||||
let clientHeight = 0;
|
||||
const { showFooter, isUseTabsRouter, showBreadcrumb } = settingStore;
|
||||
const headerHeight = parseFloat(sizeXxxl);
|
||||
const navDom = document.querySelector('.t-tabs__nav');
|
||||
const navHeight = isUseTabsRouter ? getOuterHeight(navDom) : 0;
|
||||
const breadcrumbDom = document.querySelector('.t-breadcrumb');
|
||||
const breadcrumbHeight = showBreadcrumb ? getOuterHeight(breadcrumbDom) : 0;
|
||||
const contentPadding = parseFloat(paddingTBXxl) * 2;
|
||||
const footerDom = document.querySelector('.t-layout__footer');
|
||||
const footerHeight = showFooter ? getOuterHeight(footerDom) : 0;
|
||||
const top = headerHeight + navHeight + breadcrumbHeight + contentPadding + footerHeight + 2;
|
||||
heightRef.value = window.innerHeight - top;
|
||||
clientHeight = document.documentElement.clientHeight - top;
|
||||
iframe.style.height = `${clientHeight}px`;
|
||||
}
|
||||
|
||||
function hideLoading() {
|
||||
loading.value = false;
|
||||
calcHeight();
|
||||
}
|
||||
|
||||
useWindowSizeFn(calcHeight, { immediate: true });
|
||||
|
||||
watch(
|
||||
[() => settingStore.showFooter, () => settingStore.isUseTabsRouter, () => settingStore.showBreadcrumb],
|
||||
debounce(calcHeight, 250),
|
||||
);
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
@prefix-cls: ~'@{starter-prefix}-iframe-page';
|
||||
|
||||
.@{prefix-cls} {
|
||||
&__mask {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&__main {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background-color: #fff;
|
||||
border: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -2,7 +2,7 @@
|
|||
<div>
|
||||
<template v-for="item in list" :key="item.path">
|
||||
<template v-if="!item.children || !item.children.length || item.meta?.single">
|
||||
<t-menu-item v-if="getHref(item)" :href="getHref(item)?.[0]" :name="item.path" :value="getPath(item)">
|
||||
<t-menu-item v-if="getHref(item)" :name="item.path" :value="getPath(item)" @click="openHref(getHref(item)[0])">
|
||||
<template #icon>
|
||||
<t-icon v-if="beIcon(item)" :name="item.icon" />
|
||||
<component :is="beRender(item).render" v-else-if="beRender(item).can" class="t-icon" />
|
||||
|
@ -72,7 +72,11 @@ const getMenuList = (list: MenuRoute[], basePath?: string): MenuRoute[] => {
|
|||
};
|
||||
|
||||
const getHref = (item: MenuRoute) => {
|
||||
return item.path.match(/(http|https):\/\/([\w.]+\/?)\S*/);
|
||||
const { frameSrc, frameBlank } = item.meta;
|
||||
if (frameSrc && frameBlank) {
|
||||
return frameSrc.match(/(http|https):\/\/([\w.]+\/?)\S*/);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const getPath = (item) => {
|
||||
|
@ -98,6 +102,10 @@ const beRender = (item: MenuRoute) => {
|
|||
render: null,
|
||||
};
|
||||
};
|
||||
|
||||
const openHref = (url: string) => {
|
||||
window.open(url);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
|
|
25
src/layouts/frame/index.vue
Normal file
25
src/layouts/frame/index.vue
Normal file
|
@ -0,0 +1,25 @@
|
|||
<template>
|
||||
<div v-if="showFrame">
|
||||
<template v-for="frame in getFramePages" :key="frame.path">
|
||||
<frame-content v-if="hasRenderFrame(frame.name)" v-show="showIframe(frame)" :frame-src="frame.meta.frameSrc" />
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent, unref, computed } from 'vue';
|
||||
import FrameContent from '../components/FrameContent.vue';
|
||||
|
||||
import { useFrameKeepAlive } from './useFrameKeepAlive';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'FrameLayout',
|
||||
components: { FrameContent },
|
||||
setup() {
|
||||
const { getFramePages, hasRenderFrame, showIframe } = useFrameKeepAlive();
|
||||
|
||||
const showFrame = computed(() => unref(getFramePages).length > 0);
|
||||
|
||||
return { getFramePages, hasRenderFrame, showIframe, showFrame };
|
||||
},
|
||||
});
|
||||
</script>
|
53
src/layouts/frame/useFrameKeepAlive.ts
Normal file
53
src/layouts/frame/useFrameKeepAlive.ts
Normal file
|
@ -0,0 +1,53 @@
|
|||
import { computed, toRaw, unref } from 'vue';
|
||||
import uniqBy from 'lodash/uniqBy';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useSettingStore, useTabsRouterStore } from '@/store';
|
||||
import type { MenuRoute } from '@/types/interface';
|
||||
|
||||
export function useFrameKeepAlive() {
|
||||
const router = useRouter();
|
||||
const { currentRoute } = router;
|
||||
const { isUseTabsRouter } = useSettingStore();
|
||||
const tabStore = useTabsRouterStore();
|
||||
const getFramePages = computed(() => {
|
||||
const ret = getAllFramePages(toRaw(router.getRoutes()) as unknown as MenuRoute[]) || [];
|
||||
return ret;
|
||||
});
|
||||
|
||||
const getOpenTabList = computed((): string[] => {
|
||||
return tabStore.tabRouters.reduce((prev: string[], next) => {
|
||||
if (next.meta && Reflect.has(next.meta, 'frameSrc')) {
|
||||
prev.push(next.name as string);
|
||||
}
|
||||
return prev;
|
||||
}, []);
|
||||
});
|
||||
|
||||
function getAllFramePages(routes: MenuRoute[]): MenuRoute[] {
|
||||
let res: MenuRoute[] = [];
|
||||
for (const route of routes) {
|
||||
const { meta: { frameSrc, frameBlank } = {}, children } = route;
|
||||
if (frameSrc && !frameBlank) {
|
||||
res.push(route);
|
||||
}
|
||||
if (children && children.length) {
|
||||
res.push(...getAllFramePages(children));
|
||||
}
|
||||
}
|
||||
res = uniqBy(res, 'name');
|
||||
return res;
|
||||
}
|
||||
|
||||
function showIframe(item: MenuRoute) {
|
||||
return item.name === unref(currentRoute).name;
|
||||
}
|
||||
|
||||
function hasRenderFrame(name: string) {
|
||||
if (!unref(isUseTabsRouter)) {
|
||||
return router.currentRoute.value.name === name;
|
||||
}
|
||||
return unref(getOpenTabList).includes(name);
|
||||
}
|
||||
|
||||
return { hasRenderFrame, getFramePages, showIframe, getAllFramePages };
|
||||
}
|
|
@ -56,7 +56,7 @@ const appendNewRoute = () => {
|
|||
meta: { title },
|
||||
name,
|
||||
} = route;
|
||||
tabsRouterStore.appendTabRouterList({ path, query, title: title as string, name, isAlive: true });
|
||||
tabsRouterStore.appendTabRouterList({ path, query, title: title as string, name, isAlive: true, meta: route.meta });
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
|
|
47
src/router/modules/iframe.ts
Normal file
47
src/router/modules/iframe.ts
Normal file
|
@ -0,0 +1,47 @@
|
|||
import Layout from '@/layouts/index.vue';
|
||||
|
||||
const IFrame = () => import('@/layouts/components/FrameBlank.vue');
|
||||
|
||||
export default [
|
||||
{
|
||||
path: '/frame',
|
||||
name: 'Frame',
|
||||
component: Layout,
|
||||
redirect: '/frame/doc',
|
||||
meta: {
|
||||
icon: 'internet',
|
||||
title: '外部页面',
|
||||
},
|
||||
|
||||
children: [
|
||||
{
|
||||
path: 'doc',
|
||||
name: 'Doc',
|
||||
component: IFrame,
|
||||
meta: {
|
||||
frameSrc: 'https://tdesign.tencent.com/starter/docs/vue-next/get-started',
|
||||
title: '使用文档(内嵌)',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'TDesign',
|
||||
name: 'TDesign',
|
||||
component: IFrame,
|
||||
meta: {
|
||||
frameSrc: 'https://tdesign.tencent.com/vue-next/getting-started',
|
||||
title: 'TDesign 文档(内嵌)',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'TDesign2',
|
||||
name: 'TDesign2',
|
||||
component: IFrame,
|
||||
meta: {
|
||||
frameSrc: 'https://tdesign.tencent.com/vue-next/getting-started',
|
||||
frameBlank: true,
|
||||
title: 'TDesign 文档(外链)',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
2
src/types/interface.d.ts
vendored
2
src/types/interface.d.ts
vendored
|
@ -4,6 +4,7 @@ import STYLE_CONFIG from '@/config/style';
|
|||
export interface MenuRoute {
|
||||
path: string;
|
||||
title?: string;
|
||||
name?: string;
|
||||
icon?:
|
||||
| string
|
||||
| {
|
||||
|
@ -42,6 +43,7 @@ export interface TRouterInfo {
|
|||
name?: RouteRecordName;
|
||||
isAlive?: boolean;
|
||||
isHome?: boolean;
|
||||
meta?: any;
|
||||
}
|
||||
|
||||
export interface TTabRouterType {
|
||||
|
|
Loading…
Reference in New Issue
Block a user