UNPKG

@ithinkdt/naive

Version:

iThinkDT Naive UI

393 lines (364 loc) 13.2 kB
import { defineComponent, reactive, computed, ref, watch, inject } from 'vue' import { useRouter } from 'vue-router' import { NTooltip, NButton, NIcon, NTag, NDropdown } from 'ithinkdt-ui' import { promiseTimeout, useElementHover, useEventListener, watchDebounced } from '@vueuse/core' import { useI18n } from '@ithinkdt/core' import { c, cB, cE, CSS_MOUNT_ANCHOR_META_NAME, CSS_STYLE_PREFIX as p, flexGap, fullHeight } from '@ithinkdt/core/cssr' import { IClose, ICloseLeft, ICloseRight, ICloseOther, IReload, ICircle, ITabFull, ITabNormal, IOpen, } from '../assets.jsx' const renderIcon = (I, size = 20) => ( <NIcon size={size}> <I /> </NIcon> ) export const DtTabs = defineComponent({ name: 'DtTabs', props: { showBreadcrumb: Boolean }, setup(props) { const cls = `${p}-page-tabs` createStyle(cls) const router = useRouter() const theme = inject('__INJECTED_THEME__') const { $t, t } = useI18n() const ctx = reactive({ visible: false, x: 0, y: 0 }) const onCtx = (e, tab) => { e.preventDefault() e.stopPropagation() showCtx(tab, e.clientX, e.clientY) } const showCtx = async (tab, x, y) => { if (ctx.visible && ctx.tab === tab) return ctx.tab = tab if (ctx.visible) { ctx.visible = false await promiseTimeout(130) } ctx.visible = true ctx.x = x ctx.y = y } const onClickoutside = async () => { requestIdleCallback( () => { ctx.visible = false }, { timeout: 30 }, ) } const notCurrTab = computed(() => !ctx.tab || ctx.tab !== theme.currentPage) const notTab = computed(() => !ctx.tab) const options = reactive([ { key: 'reload', label: $t('sys.tab.reload'), disabled: notCurrTab, icon: () => renderIcon(IReload), }, { type: 'divider', key: 'd1', }, { key: 'open', label: $t('sys.tab.openWindow'), disabled: false, icon: () => renderIcon(IOpen), }, { type: 'divider', key: 'd1', }, { key: 'closeAll', label: $t('sys.tab.closeAll'), disabled: false, icon: () => renderIcon(IClose), }, { key: 'closeLeft', label: $t('sys.tab.closeLeft'), disabled: notTab, icon: () => renderIcon(ICloseLeft), }, { key: 'closeRight', label: $t('sys.tab.closeRight'), disabled: notTab, icon: () => renderIcon(ICloseRight), }, { key: 'closeOther', label: $t('sys.tab.closeOther'), disabled: notTab, icon: () => renderIcon(ICloseOther), }, ]) const onSelect = (key) => { ctx.visible = false switch (key) { case 'open': { window.open(ctx.tab.href, '_blank') break } case 'reload': { theme.reloadPage() break } case 'close': { theme.closePage(ctx.tab) break } case 'closeOther': { theme.closePages(theme.pages.filter((tab) => tab !== ctx.tab)) break } case 'closeLeft': { theme.closePages(theme.pages.slice(0, theme.pages.indexOf(ctx.tab))) theme.pages = theme.pages.slice(theme.pages.indexOf(ctx.tab)) break } case 'closeRight': { theme.closePages(theme.pages.slice(theme.pages.indexOf(ctx.tab) + 1)) break } case 'closeAll': { theme.closeAllPages() break } } } function scroll2Tab(index) { if (!content.value?.children.length) return const target = content.value.children[index] el.value.scrollTo({ behavior: 'smooth', left: Math.max( 0, content.value.offsetLeft + target.offsetLeft - el.value.clientWidth / 2 + target.clientWidth / 2 + 10, ), }) } const el = ref() const content = ref() watchDebounced( useElementHover(el), (hoverd) => { if (!hoverd) scroll2Tab(theme.currentPageIndex) }, { debounce: 666 }, ) watch(() => theme.currentPageIndex, scroll2Tab, { flush: 'post' }) useEventListener( el, 'wheel', (e) => { if (e.deltaMode !== WheelEvent.DOM_DELTA_PIXEL) { return } e.preventDefault() el.value.scrollBy({ left: e.deltaY, }) }, { passive: false }, ) const Suffix = ( <div class={`${cls}__suffix`}> <NTooltip placement="bottom" delay={333}> {{ default: () => (theme.fullTab ? t('sys.tab.exit') : t('sys.tab.full')), trigger: () => ( <NButton text onClick={() => (theme.fullTab = !theme.fullTab)}> {{ icon: () => <NIcon>{theme.fullTab ? <ITabNormal /> : <ITabFull />}</NIcon>, }} </NButton> ), }} </NTooltip> <NTooltip placement="bottom" delay={333}> {{ default: () => t('sys.tab.reload'), trigger: () => ( <NButton text onClick={() => theme.reloadPage()}> {{ icon: () => ( <NIcon size={20}> <IReload /> </NIcon> ), }} </NButton> ), }} </NTooltip> <NTooltip placement="bottom" delay={333}> {{ default: () => t('sys.tab.closeAll'), trigger: () => ( <NButton text onClick={() => onSelect('closeAll')}> {{ icon: () => ( <NIcon> <IClose /> </NIcon> ), }} </NButton> ), }} </NTooltip> </div> ) const DtTab = defineComponent({ name: 'DtTabItem', props: { current: Boolean, showBreadcrumb: Boolean, index: { type: Number, required: true }, tab: { type: Object, required: true }, }, setup(props) { const auth = inject('__INJECTED_AUTH__') const theme = inject('__INJECTED_THEME__') const slots = inject('__INJECT_SLOTS__', {}) return () => ( <NTag class={[`${cls}__tab ${props.current ? cls + '__tab--curr' : ''}`]} type={props.current ? 'primary' : 'default'} closable bordered={false} onClose={() => theme.closePageByIndex(props.index)} onClick={() => router.push(props.tab.path)} onContextmenu={(e) => onCtx(e, props.tab)} > {{ default: () => { const text = props.tab.title ?? auth.moduleMap[props.tab.moduleKey]?.label ?? props.tab.path return props.current && theme.hasBreadcrumb && props.showBreadcrumb && slots.breadcrumb ? ( <span style="pointer-events: none">{slots.breadcrumb()}</span> ) : ( text ) }, icon: () => props.current ? ( <NIcon size={10}> <ICircle /> </NIcon> ) : undefined, }} </NTag> ) }, }) return () => { return ( <div class={[cls, { [`${cls}--dark`]: theme.isDark }]} ref={el}> <div class={`${cls}__prefix`} /> <div class={`${cls}__content`} ref={content}> {theme.pages.map((tab, i) => { return ( <DtTab key={tab.key} index={i} current={theme.currentPageIndex === i} tab={tab} showBreadcrumb={props.showBreadcrumb} /> ) })} </div> {Suffix} <NDropdown placement="bottom-start" size="small" trigger="manual" show={ctx.visible} x={ctx.x} y={ctx.y} options={options} onClickoutside={onClickoutside} onSelect={onSelect} /> </div> ) } }, }) let style function createStyle(cls) { if (!style) { style = cB( 'page-tabs', { ...fullHeight, display: 'flex', overflow: 'hidden', }, [ cE('prefix', { zIndex: '1', flex: '0 0 16px', position: 'sticky', left: '0', backdropFilter: 'blur(2px)', pointerEvents: 'none', }), cE( 'suffix', { ...flexGap('14px'), width: '32px', flexDirection: 'row-reverse', opacity: '0.8', transition: 'width 0.15s cubic-bezier(0.4, 0, 0.2, 1)', overflow: 'hidden', padding: '3px 23px 0 0', zIndex: '1', flex: '0 0 auto', position: 'sticky', right: '0', backdropFilter: 'blur(2px)', }, [c('&:hover', { width: '100px' })], ), cE('content', { ...flexGap('10px'), ...fullHeight, flex: '1 0 auto', alignItems: 'center', }), cE('tab', { cursor: 'pointer', padding: '4px 12px', height: '33px', '--n-close-margin': '0 0 0 8px !important', '--n-close-icon-size': '12px !important', '--n-font-size': '14px !important', }), ], ) style.mount({ id: cls, anchorMetaName: CSS_MOUNT_ANCHOR_META_NAME, }) } }