@ithinkdt/naive
Version:
iThinkDT Naive UI
393 lines (364 loc) • 13.2 kB
JSX
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,
})
}
}