first commit

This commit is contained in:
ye1023
2026-04-09 15:09:34 +08:00
commit d8e3a63df1
2246 changed files with 352109 additions and 0 deletions
@@ -0,0 +1,40 @@
<template>
<span :class="`${prefixCls}__extra-fold`" @click="handleFold">
<Icon :icon="getIcon" />
</span>
</template>
<script lang="ts">
import { defineComponent, unref, computed } from 'vue';
import { Icon } from '/@/components/Icon';
import { useDesign } from '/@/hooks/web/useDesign';
import { useHeaderSetting } from '/@/hooks/setting/useHeaderSetting';
import { useMenuSetting } from '/@/hooks/setting/useMenuSetting';
import { triggerWindowResize } from '/@/utils/event';
export default defineComponent({
name: 'FoldButton',
components: { Icon },
setup() {
const { prefixCls } = useDesign('multiple-tabs-content');
const { getShowMenu, setMenuSetting } = useMenuSetting();
const { getShowHeader, setHeaderSetting } = useHeaderSetting();
const getIsUnFold = computed(() => !unref(getShowMenu) && !unref(getShowHeader));
const getIcon = computed(() => (unref(getIsUnFold) ? 'codicon:screen-normal' : 'codicon:screen-full'));
function handleFold() {
const isUnFold = unref(getIsUnFold);
setMenuSetting({
show: isUnFold,
hidden: !isUnFold,
});
setHeaderSetting({ show: isUnFold });
triggerWindowResize();
}
return { prefixCls, getIcon, handleFold };
},
});
</script>
@@ -0,0 +1,112 @@
<template>
<Dropdown :dropMenuList="getDropMenuList" :trigger="getTrigger" @menuEvent="handleMenuEvent" :overlayClassName="prefixCls">
<div :class="`${prefixCls}__info`" @contextmenu="handleContext" v-if="getIsTabs">
<!-- updateBy:sunjianlei---updateDate:2021-09-03---修改tab切换栏样式增加前缀图标 -->
<!-- <span v-if="showPrefixIcon" :class="`${prefixCls}__prefix-icon`" @click="handleContext">
<Icon :icon="prefixIconType" :size="14" />
</span> -->
<span class="ml-1">{{ getTitle }}</span>
</div>
<span :class="`${prefixCls}__extra-quick`" v-else @click="handleContext">
<Icon icon="ion:chevron-down" />
</span>
</Dropdown>
</template>
<script lang="ts">
import type { PropType } from 'vue';
import type { RouteLocationNormalized } from 'vue-router';
import { defineComponent, computed, unref } from 'vue';
import { Dropdown } from '/@/components/Dropdown/index';
import { Icon } from '/@/components/Icon';
import { TabContentProps } from '../types';
import { TabsThemeEnum } from '/@/enums/appEnum';
import { useDesign } from '/@/hooks/web/useDesign';
import { useI18n } from '/@/hooks/web/useI18n';
import { useTabDropdown } from '../useTabDropdown';
import { useMultipleTabSetting } from '/@/hooks/setting/useMultipleTabSetting';
import { useLocaleStore } from '/@/store/modules/locale';
export default defineComponent({
name: 'TabContent',
components: { Dropdown, Icon },
props: {
tabItem: {
type: Object as PropType<RouteLocationNormalized>,
default: null,
},
isExtra: Boolean,
},
setup(props) {
const { prefixCls } = useDesign('multiple-tabs-content');
const { t } = useI18n();
//update-begin-author:taoyan date:2022-6-1 for: VUEN-1144 online 配置成菜单后,打开菜单,显示名称未展示为菜单名称
const localeStore = useLocaleStore();
const getTitle = computed(() => {
const { tabItem: { meta, fullPath } = {} } = props;
let title = localeStore.getPathTitle(fullPath);
if (title) {
return title;
}
return meta && t(meta.title as string);
});
//update-end-author:taoyan date:2022-6-1 for: VUEN-1144 online 配置成菜单后,打开菜单,显示名称未展示为菜单名称
const getIsTabs = computed(() => !props.isExtra);
// updateBy:sunjianlei---updateDate:2021-09-03---修改tab切换栏样式:前缀图标类型
const prefixIconType = computed(() => {
if (props.tabItem.meta.icon) {
return props.tabItem.meta.icon;
} else if (props.tabItem.path === '/dashboard/analysis') {
// 当是首页时返回 home 图标 TODO 此处可能需要动态判断首页路径
return 'ant-design:home-outlined';
} else {
return 'ant-design:code';
}
});
const getTrigger = computed((): ('contextmenu' | 'click' | 'hover')[] => (unref(getIsTabs) ? ['contextmenu'] : ['click']));
const { getDropMenuList, handleMenuEvent, handleContextMenu } = useTabDropdown(props as TabContentProps, getIsTabs);
function handleContext(e) {
props.tabItem && handleContextMenu(props.tabItem)(e);
}
const { getTabsTheme } = useMultipleTabSetting();
// 是否显示图标
const showPrefixIcon = computed(() => unref(getTabsTheme) === TabsThemeEnum.SMOOTH);
return {
prefixCls,
getDropMenuList,
handleMenuEvent,
handleContext,
getTrigger,
getIsTabs,
getTitle,
prefixIconType,
showPrefixIcon,
};
},
});
</script>
<style lang="less">
@prefix-cls: ~'@{namespace}-multiple-tabs-content';
.@{prefix-cls} {
.ant-dropdown-menu-item {
.ant-dropdown-menu-title-content {
.anticon {
font-size: 14px !important;
}
span:not(.anticon) {
margin-left: 6px;
}
}
}
}
</style>
@@ -0,0 +1,32 @@
<template>
<span :class="`${prefixCls}__extra-redo`" @click="handleRedo">
<SvgIcon name="reload-01"></SvgIcon>
</span>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue';
import { useDesign } from '/@/hooks/web/useDesign';
import { useTabs } from '/@/hooks/web/useTabs';
import { SvgIcon } from '/@/components/Icon/index';
export default defineComponent({
name: 'TabRedo',
components: { SvgIcon },
setup() {
const loading = ref(false);
const { prefixCls } = useDesign('multiple-tabs-content');
const { refreshPage } = useTabs();
async function handleRedo() {
loading.value = true;
await refreshPage();
setTimeout(() => {
loading.value = false;
// Animation execution time
}, 1200);
}
return { prefixCls, handleRedo, loading };
},
});
</script>
+228
View File
@@ -0,0 +1,228 @@
@prefix-cls: ~'@{namespace}-multiple-tabs';
html[data-theme='dark'] {
.@{prefix-cls} {
.ant-tabs-tab {
border-bottom: 1px solid @border-color-base;
}
}
}
html[data-theme='light'] {
.@{prefix-cls} {
.ant-tabs-tab:not(.ant-tabs-tab-active) {
border: 1px solid #e6e6e6;
}
}
}
.@{prefix-cls} {
z-index: 10;
height: @multiple-height + 2;
line-height: @multiple-height + 2;
background-color: @component-background;
border-bottom: 1px solid @border-color-base;
box-shadow: 0 4px 4px rgb(0 21 41 / 8%);
.ant-tabs-small {
height: calc(@multiple-height + 4px);
}
.ant-tabs.ant-tabs-card {
padding-left: 0px;
.ant-tabs-nav {
height: calc(@multiple-height);
margin: 0;
background-color: @component-background;
border: 0;
box-shadow: none;
.ant-tabs-nav-wrap {
height: @multiple-height;
margin-top: 2px;
}
.ant-tabs-tab {
height: calc(@multiple-height - 4px);
padding-right: 12px;
line-height: calc(@multiple-height - 4px);
color: @text-color-base;
background-color: @component-background;
transition: none;
.ant-tabs-tab-btn {
color: @text-color-base;
transition: none;
}
&:hover {
.ant-tabs-tab-remove .anticon-close {
opacity: 1;
}
}
.ant-tabs-tab-remove {
margin: 0;
padding: 0;
position: relative;
top: 0;
left: 4px;
.anticon-close {
width: 8px;
height: 12px;
font-size: 12px;
color: inherit;
opacity: 0;
transition: none;
&:hover {
svg {
width: 0.8em;
}
}
}
}
> div {
display: flex;
justify-content: center;
align-items: center;
}
svg {
fill: @text-color-base;
}
}
.ant-tabs-tab:not(.ant-tabs-tab-active) {
&:hover {
color: @primary-color;
}
}
.ant-tabs-tab-active {
position: relative;
padding-left: 18px;
color: @white !important;
background: @primary-color;
border: 1px solid transparent;
transition: none;
.ant-tabs-tab-btn {
color: @white;
}
.ant-tabs-tab-remove .anticon-close {
opacity: 1;
}
svg {
width: 0.7em;
fill: @white;
}
}
}
.ant-tabs-nav > div:nth-child(1) {
padding: 0 6px;
.ant-tabs-tab {
margin-right: 6px !important;
}
}
}
.ant-tabs-tab:not(.ant-tabs-tab-active) {
.ant-tabs-tab-remove .anticon-close {
font-size: 12px;
svg {
width: 0.6em;
}
}
}
.ant-tabs-extra-content {
// update-begin--author:liaozhiyang---date:20241016---for:【issues/7345】标签样式切换到极简模式样式错乱
// margin-top: 2px;
// update-end--author:liaozhiyang---date:20241016---for:【issues/7345】标签样式切换到极简模式样式错乱
line-height: @multiple-height !important;
}
.ant-dropdown-trigger {
display: inline-flex;
}
&--hide-close {
.ant-tabs-tab-remove .anticon-close {
opacity: 0 !important;
}
}
&-content {
&__extra-quick,
&__extra-redo,
&__extra-fold {
display: inline-block;
width: 36px;
height: @multiple-height;
line-height: @multiple-height;
color: @text-color-secondary;
text-align: center;
cursor: pointer;
border-left: 1px solid @border-color-base;
&:hover {
color: @text-color-base;
}
span[role='img'] {
transform: rotate(90deg);
}
}
&__extra-redo {
span[role='img'] {
transform: rotate(0deg);
}
}
&__info {
display: inline-block;
width: 100%;
height: @multiple-height - 2;
padding-left: 0;
margin-left: -10px;
font-size: 12px;
cursor: pointer;
user-select: none;
}
}
}
.ant-tabs-dropdown-menu {
&-title-content {
display: flex;
align-items: center;
.@{prefix-cls} {
&-content__info {
width: auto;
margin-left: 0;
line-height: 28px;
}
}
}
&-item-remove {
margin-left: auto;
}
}
.multiple-tabs__dropdown {
.ant-dropdown-content {
width: 172px;
}
}
+192
View File
@@ -0,0 +1,192 @@
<template>
<div :class="getWrapClass">
<Tabs
type="editable-card"
size="small"
:animated="false"
:hideAdd="true"
:tabBarGutter="3"
:activeKey="activeKeyRef"
@change="handleChange"
@edit="handleEdit"
>
<template v-for="item in getTabsState" :key="item.query ? item.fullPath : item.path">
<TabPane :closable="!(item && item.meta && item.meta.affix)">
<template #tab>
<TabContent :tabItem="item" />
</template>
</TabPane>
</template>
<template #rightExtra v-if="getShowRedo || getShowQuick">
<div class="rightExtra">
<TabRedo v-if="getShowRedo" />
<!-- <TabContent isExtra :tabItem="$route" v-if="getShowQuick" /> -->
<!-- 列表页全屏
<FoldButton v-if="getShowFold" />-->
<!-- <FullscreenOutlined /> -->
<router-link to="/ai" class="ai-icon">
<a-tooltip title="AI助手" placement="left">
<!-- <svg t="1706259688149" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2056" width="17" height="17">-->
<!-- <path d="M826.368 325.632c0-7.168 2.048-10.24 10.24-10.24h123.904c7.168 0 10.24 2.048 10.24 10.24v621.568c0 7.168-2.048 10.24-10.24 10.24h-122.88c-8.192 0-10.24-4.096-10.24-10.24l-1.024-621.568z m-8.192-178.176c0-50.176 35.84-79.872 79.872-79.872 48.128 0 79.872 32.768 79.872 79.872 0 52.224-33.792 79.872-81.92 79.872-46.08 1.024-77.824-27.648-77.824-79.872zM462.848 584.704C441.344 497.664 389.12 307.2 368.64 215.04h-2.048c-16.384 92.16-58.368 247.808-92.16 369.664h188.416zM243.712 712.704l-62.464 236.544c-2.048 7.168-4.096 8.192-12.288 8.192H54.272c-8.192 0-10.24-2.048-8.192-12.288l224.256-783.36c4.096-13.312 7.168-26.624 8.192-65.536 0-6.144 2.048-8.192 7.168-8.192H450.56c6.144 0 8.192 2.048 10.24 8.192l250.88 849.92c2.048 7.168 0 10.24-7.168 10.24H573.44c-7.168 0-10.24-2.048-12.288-7.168l-65.536-236.544c1.024 1.024-251.904 0-251.904 0z" fill="#333333" p-id="19816"></path>-->
<!-- </svg>-->
<svg t="1737024931936" class="icon" viewBox="0 0 3011 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4248" width="256" height="256"><path d="M2028.242824 558.561882h415.021176V445.620706h-346.352941V321.415529h346.352941V210.823529l-328.463059 10.842353a2367.969882 2367.969882 0 0 0-31.021176-127.09647c341.955765-4.397176 602.352941-13.492706 781.131294-27.286588l41.441882 124.265411-320.933647 14.095059v115.772235h307.802353v124.205177h-307.802353v112.941176h375.506824v127.096471h-375.506824v113.844706c0 30.117647-5.782588 56.982588-17.468235 80.474353a114.447059 114.447059 0 0 1-50.778353 53.187764c-22.287059 11.926588-41.261176 19.154824-56.922353 21.684706-15.36 2.770824-82.522353 5.300706-201.426824 7.529412a5065.065412 5065.065412 0 0 0-31.984941-142.155294c64.632471 4.999529 114.447059 7.529412 149.624471 7.529412 44.574118 0 66.861176-23.190588 66.861176-69.632v-72.463059h-415.081411v-127.096471zM1685.624471 956.717176a1296.865882 1296.865882 0 0 0-27.286589-133.662117 2187.745882 2187.745882 0 0 0 96.978824 3.794823c18.191059 0 32.587294-4.758588 43.248941-14.155294 10.661647-9.697882 17.468235-23.491765 20.239059-41.381647 3.132235-17.889882 6.023529-66.379294 8.432941-145.408 2.590118-79.390118 3.614118-179.621647 3.373177-300.754823h-109.206589c-3.734588 212.389647-24.455529 364.604235-62.102588 456.523294-37.345882 91.919059-77.161412 156.491294-119.567059 193.837176-28.551529-27.888941-59.632941-54.392471-93.123764-79.510588-142.757647 12.830118-265.456941 25.298824-368.037648 37.165176l-15.962352-126.132705c14.095059-0.602353 28.190118-1.385412 42.345411-2.349177V88.003765h377.374118v687.043764c12.227765-1.204706 24.154353-2.349176 35.779765-3.312941 50.838588-95.653647 77.040941-244.555294 78.607058-446.58447h-85.172705V200.944941h87.04c0.301176-47.706353 0.481882-100.412235 0.481882-158.117647h125.168941c0 58.368-0.301176 111.073882-0.963765 158.117647H1957.647059l-9.878588 525.613177c-1.566118 62.464-5.481412 104.688941-11.745883 126.614588-6.324706 22.287059-16.805647 41.441882-31.563294 57.404235-14.757647 16.022588-32.768 27.105882-54.091294 33.430588-21.082353 6.264471-75.896471 10.480941-164.743529 12.649412z m-321.837177-759.567058h-140.227765V327.077647h140.227765V197.150118z m-140.227765 359.604706h140.227765V426.767059h-140.227765v129.867294z m140.227765 229.616941v-129.92753h-140.227765v140.288l140.227765-10.36047zM963.764706 114.326588V843.294118h-155.286588V114.326588H963.764706zM716.679529 843.294118H547.297882l-54.573176-167.032471H227.689412L174.140235 843.294118H5.662118l266.842353-728.96753h182.512941L716.739765 843.294118zM455.981176 556.212706L373.157647 303.585882c-5.300706-16.022588-9.035294-37.165176-11.264-63.548235h-4.216471a269.010824 269.010824 0 0 1-12.709647 61.680941L261.662118 556.212706H455.981176z" fill="#1F1F1F" p-id="4249"></path></svg>
</a-tooltip>
</router-link>
</div>
</template>
</Tabs>
</div>
</template>
<script lang="ts">
import type { RouteLocationNormalized, RouteMeta } from 'vue-router';
import { defineComponent, computed, unref, ref } from 'vue';
import { Tabs } from 'ant-design-vue';
import TabContent from './components/TabContent.vue';
import FoldButton from './components/FoldButton.vue';
import TabRedo from './components/TabRedo.vue';
import { useGo } from '/@/hooks/web/usePage';
import { useMultipleTabStore } from '/@/store/modules/multipleTab';
import { useUserStore } from '/@/store/modules/user';
import { initAffixTabs, useTabsDrag } from './useMultipleTabs';
import { useDesign } from '/@/hooks/web/useDesign';
import { useMultipleTabSetting } from '/@/hooks/setting/useMultipleTabSetting';
import { REDIRECT_NAME } from '/@/router/constant';
import { listenerRouteChange } from '/@/logics/mitt/routeChange';
import { useRouter } from 'vue-router';
import Aide from "/@/views/dashboard/ai/components/aide/index.vue"
export default defineComponent({
name: 'MultipleTabs',
components: {
TabRedo,
FoldButton,
Tabs,
TabPane: Tabs.TabPane,
TabContent,
Aide,
},
setup() {
const affixTextList = initAffixTabs();
const activeKeyRef = ref('');
useTabsDrag(affixTextList);
const tabStore = useMultipleTabStore();
const userStore = useUserStore();
const router = useRouter();
const { prefixCls } = useDesign('multiple-tabs');
const go = useGo();
const { getShowQuick, getShowRedo, getShowFold, getTabsTheme } = useMultipleTabSetting();
const getTabsState = computed(() => {
return tabStore.getTabList.filter((item) => !item.meta?.hideTab);
});
const unClose = computed(() => unref(getTabsState).length === 1);
const getWrapClass = computed(() => {
return [
prefixCls,
{
[`${prefixCls}--hide-close`]: unref(unClose),
},
`${prefixCls}--theme-${unref(getTabsTheme)}`,
];
});
listenerRouteChange((route) => {
const { name } = route;
if (name === REDIRECT_NAME || !route || !userStore.getToken) {
return;
}
const { path, fullPath, meta = {} } = route;
const { currentActiveMenu, hideTab } = meta as RouteMeta;
const isHide = !hideTab ? null : currentActiveMenu;
const p = isHide || fullPath || path;
if (activeKeyRef.value !== p) {
activeKeyRef.value = p as string;
}
if (isHide) {
const findParentRoute = router.getRoutes().find((item) => item.path === currentActiveMenu);
findParentRoute && tabStore.addTab(findParentRoute as unknown as RouteLocationNormalized);
} else {
tabStore.addTab(unref(route));
}
});
function handleChange(activeKey: any) {
activeKeyRef.value = activeKey;
go(activeKey, false);
}
// Close the current tab
function handleEdit(targetKey: string) {
// Added operation to hide, currently only use delete operation
if (unref(unClose)) {
return;
}
tabStore.closeTabByKey(targetKey, router);
}
return {
prefixCls,
unClose,
getWrapClass,
handleEdit,
handleChange,
activeKeyRef,
getTabsState,
getShowQuick,
getShowRedo,
getShowFold,
};
},
});
</script>
<style lang="less">
@import './index.less';
@import './tabs.theme.card.less';
@import './tabs.theme.smooth.less';
</style>
<style lang="less" scoped>
@prefix-cls: ~'@{namespace}-multiple-tabs';
.@{prefix-cls} {
:deep(.anticon) {
display: inline-block;
}
// update-begin--author:liaozhiyang---date:20241016---for:【issues/7345】标签样式切换到极简模式样式错乱
.rightExtra {
display: flex;
:deep(svg) {
&:not(.icon) {
vertical-align: -0.3em;
}
}
.ai-icon {
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
width: 50px;
padding: 0 4px;
height: 50px;
color: @text-color;
text-align: center;
border-left: 1px solid @border-color-base;
overflow: hidden;
}
}
// update-end--author:liaozhiyang---date:20241016---for:【issues/7345】标签样式切换到极简模式样式错乱
}
</style>
@@ -0,0 +1,236 @@
// tabs卡片样式
@prefix-cls-theme-card: ~'@{prefix-cls}.@{prefix-cls}--theme-card';
html[data-theme='dark'] {
.@{prefix-cls-theme-card} {
.ant-tabs-tab {
border-top: none !important;
border-left: none !important;
border-right: none !important;
}
}
}
html[data-theme='light'] {
.@{prefix-cls-theme-card} {
.ant-tabs-tab:not(.ant-tabs-tab-active) {
border-top: none !important;
border-left: none !important;
border-right: none !important;
}
}
}
.@{prefix-cls-theme-card} {
@tabHeight: calc(@multiple-card-height - 10px);
z-index: 10;
height: @multiple-card-height;
line-height: @multiple-card-height;
background-color: @component-background;
box-shadow: 0 1px 4px rgb(0 21 41 / 8%);
.ant-tabs-small {
height: @multiple-card-height;
}
.ant-tabs.ant-tabs-card {
.ant-tabs-nav {
height: @multiple-card-height;
margin: 0;
background-color: @component-background;
border: 0;
box-shadow: none;
padding-left: 10px;
.ant-tabs-nav-wrap {
height: @tabHeight;
margin-top: 4px;
padding-top: 0;
}
.ant-tabs-tab {
height: @tabHeight;
line-height: @tabHeight;
color: @text-color-base;
background-color: @component-background;
padding: 0 20px 0 30px;
margin: 0 10px 0 0 !important;
.ant-tabs-tab-btn {
color: @text-color-call-out;
}
&:hover {
//padding: 0 36px 0 30px;
.ant-tabs-tab-remove .anticon-close {
opacity: 1;
&:hover {
color: #fff;
background-color: #c0c4cc;
}
}
}
.ant-tabs-tab-remove {
//update-begin---author:scott ---date:2023-08-28 for:【QQYUN-6374】UnoCSS替代windicss导致应用样式问题--
/* top: 5px;*/
//update-end---author:scott ---date::2023-08-28 for:【QQYUN-6374】UnoCSS替代windicss导致应用样式问题--
left: 4px;
.anticon-close {
position: relative;
width: 14px;
height: 14px;
font-size: 13px;
color: inherit;
opacity: 0;
transition: opacity 0.15s;
top: 0;
left: 6px;
vertical-align: middle;
line-height: 10px;
overflow: hidden;
transform-origin: 100% 50%;
border-radius: 100%;
&:hover {
svg {
fill: #fff;
}
}
}
}
> div {
display: flex;
justify-content: center;
align-items: center;
}
svg {
fill: @text-color-base;
}
&:first-child {
}
}
.ant-tabs-tab:not(.ant-tabs-tab-active) {
border: none !important;
&:hover {
color: @primary-color !important;
background-color: inherit;
}
}
.ant-tabs-tab-active {
position: relative;
color: @primary-color !important;
border: 1px solid transparent;
border-bottom: 1px solid @primary-color !important;
font-weight: inherit;
.ant-tabs-tab-btn {
color: @primary-color;
}
.ant-tabs-tab-remove .anticon-close {
opacity: 0;
svg {
width: 0.6em;
}
}
svg {
width: inherit;
fill: @primary-color;
}
}
}
.ant-tabs-nav > div:nth-child(1) {
padding: 0 6px;
.ant-tabs-tab {
margin-right: 10px !important;
}
}
}
.ant-tabs-tab:not(.ant-tabs-tab-active) {
.ant-tabs-tab-remove .anticon-close {
font-size: 12px;
svg {
width: 0.6em;
}
}
}
.ant-tabs-extra-content {
position: relative;
top: 0;
line-height: @multiple-card-height !important;
}
.ant-dropdown-trigger {
display: inline-flex;
}
.@{prefix-cls}--hide-close {
.ant-tabs-tab-remove .anticon-close {
opacity: 0 !important;
}
}
.@{prefix-cls}-content {
&__extra-quick,
&__extra-redo,
&__extra-fold {
display: inline-block;
width: 36px;
height: @multiple-card-height;
line-height: @multiple-card-height;
color: @text-color-secondary;
text-align: center;
cursor: pointer;
border-left: 1px solid @border-color-base;
&:hover {
color: @text-color-base;
}
span[role='img'] {
transform: rotate(90deg);
}
}
&__extra-redo {
span[role='img'] {
transform: rotate(0deg);
}
}
&__info {
display: inline-block;
width: 100%;
height: @tabHeight;
padding-left: 0;
font-size: 14px;
cursor: pointer;
user-select: none;
}
// tab 前缀图标样式
&__prefix-icon {
& .app-iconify.anticon {
margin-right: 4px;
}
}
}
}
@@ -0,0 +1,233 @@
// tabs圆滑样式
@prefix-cls-theme-smooth: ~'@{prefix-cls}.@{prefix-cls}--theme-smooth';
html[data-theme='dark'] {
.@{prefix-cls-theme-smooth} {
.ant-tabs-tab {
border: none !important;
}
}
}
html[data-theme='light'] {
.@{prefix-cls-theme-smooth} {
.ant-tabs-tab:not(.ant-tabs-tab-active) {
border: none !important;
}
}
}
.@{prefix-cls-theme-smooth} {
@tabHeight: calc(@multiple-smooth-height - 12px);
z-index: 10;
height: @multiple-smooth-height;
line-height: @multiple-smooth-height;
background-color: @component-background;
box-shadow: 0 1px 4px rgb(0 21 41 / 8%);
.ant-tabs-small {
height: @multiple-smooth-height;
}
.ant-tabs.ant-tabs-card {
.ant-tabs-nav {
height: @multiple-smooth-height;
margin: 0;
background-color: @component-background;
border: 0;
box-shadow: none;
padding-left: 10px;
.ant-tabs-nav-wrap {
height: @tabHeight;
margin-top: 12px;
}
.ant-tabs-tab {
height: @tabHeight;
line-height: @tabHeight;
color: @text-color-base;
background-color: @component-background;
transition: padding 0.3s;
padding: 0 20px 0 26px;
margin: 0 -14px 0 0 !important;
mask: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAANoAAAAkBAMAAAAdqzmBAAAAMFBMVEVHcEwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAlTPQ5AAAAD3RSTlMAr3DvEM8wgCBA379gj5//tJBPAAAAnUlEQVRIx2NgAAM27fj/tAO/xBsYkIHyf9qCT8iWMf6nNQhAsk2f5rYheY7Dnua2/U+A28ZEe8v+F9Ax2v7/F4DbxkUH2wzgtvHTwbYPo7aN2jZq26hto7aN2jZq25Cy7Qvctnw62PYNbls9HWz7S8/G6//PsI6H4396gAUQy1je08W2jxDbpv6nD4gB2uWp+J9eYPsEhv/0BPS1DQBvoBLVZ3BppgAAAABJRU5ErkJggg==);
mask-size: 100% 100%;
position: relative;
z-index: 1;
.ant-tabs-tab-btn {
color: @text-color-base;
}
&:hover {
z-index: 2;
padding: 0 20px 0 26px;
.ant-tabs-tab-remove .anticon-close {
opacity: 1;
&:hover {
color: #fff;
background-color: #c0c4cc;
}
}
}
.ant-tabs-tab-remove {
top: -1px;
left: 8px;
.anticon-close {
position: relative;
width: 14px;
height: 14px;
font-size: 13px;
color: inherit;
opacity: 0;
transition: opacity 0.15s;
vertical-align: middle;
line-height: 10px;
overflow: hidden;
transform-origin: 100% 50%;
border-radius: 100%;
&:hover {
svg {
fill: #fff;
}
}
}
}
> div {
display: flex;
justify-content: center;
align-items: center;
}
svg {
fill: @text-color-base;
}
&:first-child {
padding: 0 30px 0 30px !important;
}
span{font-weight: 200;}
}
.ant-tabs-tab:not(.ant-tabs-tab-active) {
&:hover {
color: inherit;
background-color: #f5f5f5;
}
}
.ant-tabs-tab.ant-tabs-tab-active {
position: relative;
padding: 0 20px 0 26px;
color: @primary-color !important;
background: #f5f5f5;
border: 0;
z-index: 3;
.ant-tabs-tab-btn {
color: @primary-color;
}
.ant-tabs-tab-remove .anticon-close {
opacity: 1;
svg {
width: 0.6em;
}
}
svg {
width: inherit;
fill: @primary-color;
}
}
}
.ant-tabs-nav > div:nth-child(1) {
padding: 0 6px;
.ant-tabs-tab {
margin-right: -15px !important;
}
}
}
.ant-tabs-tab:not(.ant-tabs-tab-active) {
.anticon-close {
font-size: 12px;
svg {
width: 0.6em;
}
}
}
.ant-tabs-extra-content {
position: relative;
top: 0;
line-height: @multiple-smooth-height !important;
}
.ant-dropdown-trigger {
display: inline-flex;
}
.@{prefix-cls}--hide-close {
.ant-tabs-tab-remove .anticon-close {
opacity: 0 !important;
}
}
.@{prefix-cls}-content {
&__extra-quick,
&__extra-redo,
&__extra-fold {
display: inline-block;
width: 36px;
height: @multiple-smooth-height;
line-height: @multiple-smooth-height;
color: @text-color-secondary;
text-align: center;
cursor: pointer;
border-left: 1px solid @border-color-base;
&:hover {
color: @text-color-base;
}
span[role='img'] {
transform: rotate(90deg);
}
}
&__extra-redo {
span[role='img'] {
transform: rotate(0deg);
}
}
&__info {
display: inline-block;
width: 100%;
height: @tabHeight;
line-height: 32px;
padding-left: 0;
font-size: 14px;
cursor: pointer;
user-select: none;
}
// tab 前缀图标样式
&__prefix-icon {
& .app-iconify.anticon {
margin-right: 4px;
}
}
}
}
+25
View File
@@ -0,0 +1,25 @@
import type { DropMenu } from '/@/components/Dropdown/index';
import type { RouteLocationNormalized } from 'vue-router';
export enum TabContentEnum {
TAB_TYPE,
EXTRA_TYPE,
}
export type { DropMenu };
export interface TabContentProps {
tabItem: RouteLocationNormalized;
type?: TabContentEnum;
trigger?: ('click' | 'hover' | 'contextmenu')[];
}
export enum MenuEventEnum {
REFRESH_PAGE,
CLOSE_CURRENT,
CLOSE_LEFT,
CLOSE_RIGHT,
CLOSE_OTHER,
CLOSE_ALL,
SCALE,
}
@@ -0,0 +1,78 @@
import { toRaw, ref, nextTick } from 'vue';
import type { RouteLocationNormalized } from 'vue-router';
import { useDesign } from '/@/hooks/web/useDesign';
import { useSortable } from '/@/hooks/web/useSortable';
import { useMultipleTabStore } from '/@/store/modules/multipleTab';
import { isNullAndUnDef } from '/@/utils/is';
import projectSetting from '/@/settings/projectSetting';
import { useRouter } from 'vue-router';
export function initAffixTabs(): string[] {
const affixList = ref<RouteLocationNormalized[]>([]);
const tabStore = useMultipleTabStore();
const router = useRouter();
/**
* @description: Filter all fixed routes
*/
function filterAffixTabs(routes: RouteLocationNormalized[]) {
const tabs: RouteLocationNormalized[] = [];
routes &&
routes.forEach((route) => {
if (route.meta && route.meta.affix) {
tabs.push(toRaw(route));
}
});
return tabs;
}
/**
* @description: Set fixed tabs
*/
function addAffixTabs(): void {
const affixTabs = filterAffixTabs(router.getRoutes() as unknown as RouteLocationNormalized[]);
affixList.value = affixTabs;
for (const tab of affixTabs) {
tabStore.addTab({
meta: tab.meta,
name: tab.name,
path: tab.path,
} as unknown as RouteLocationNormalized);
}
}
let isAddAffix = false;
if (!isAddAffix) {
addAffixTabs();
isAddAffix = true;
}
return affixList.value.map((item) => item.meta?.title).filter(Boolean) as string[];
}
export function useTabsDrag(affixTextList: string[]) {
const tabStore = useMultipleTabStore();
const { multiTabsSetting } = projectSetting;
const { prefixCls } = useDesign('multiple-tabs');
nextTick(() => {
if (!multiTabsSetting.canDrag) return;
const el = document.querySelectorAll(`.${prefixCls} .ant-tabs-nav > div`)?.[0] as HTMLElement;
const { initSortable } = useSortable(el, {
filter: (e: ChangeEvent) => {
const text = e?.target?.innerText;
if (!text) return false;
return affixTextList.includes(text);
},
onEnd: (evt) => {
const { oldIndex, newIndex } = evt;
if (isNullAndUnDef(oldIndex) || isNullAndUnDef(newIndex) || oldIndex === newIndex) {
return;
}
tabStore.sortTabs(oldIndex, newIndex);
},
});
initSortable();
});
}
+169
View File
@@ -0,0 +1,169 @@
import type { TabContentProps } from './types';
import type { DropMenu } from '/@/components/Dropdown';
import type { ComputedRef } from 'vue';
import { computed, unref, reactive } from 'vue';
import { MenuEventEnum } from './types';
import { useMultipleTabStore } from '/@/store/modules/multipleTab';
import { RouteLocationNormalized, useRouter } from 'vue-router';
import { useTabs } from '/@/hooks/web/useTabs';
import { useI18n } from '/@/hooks/web/useI18n';
export function useTabDropdown(tabContentProps: TabContentProps, getIsTabs: ComputedRef<boolean>) {
const state = reactive({
current: null as Nullable<RouteLocationNormalized>,
currentIndex: 0,
});
const { t } = useI18n();
const tabStore = useMultipleTabStore();
const { currentRoute } = useRouter();
const { refreshPage, closeAll, close, closeLeft, closeOther, closeRight } = useTabs();
const getTargetTab = computed((): RouteLocationNormalized => {
return unref(getIsTabs) ? tabContentProps.tabItem : unref(currentRoute);
});
/**
* @description: drop-down list
*/
const getDropMenuList = computed(() => {
if (!unref(getTargetTab)) {
return;
}
const { meta } = unref(getTargetTab);
const { path } = unref(currentRoute);
// Refresh button
const curItem = state.current;
const isCurItem = curItem ? curItem.path === path : false;
const index = state.currentIndex;
const refreshDisabled = !isCurItem;
// update-begin--author:liaozhiyang---date:20240605---for:【TV360X-732】非当前页右键关闭左侧、关闭右侧、关闭其它功能正常使用
// Close left
const closeLeftDisabled = () => {
if (index === 0) {
return true;
} else {
// 【TV360X-1039】当只有首页和另一个tab页时关闭左侧禁用
const validTabList = tabStore.getTabList.filter((item) => !item?.meta?.affix);
return validTabList[0].path === state.current?.path;
}
};
// Close other
const closeOtherDisabled = () => {
if (tabStore.getTabList.length === 1) {
return true;
} else {
// 【TV360X-1039】当只有首页和另一个tab页时关闭其它禁用
const validTabList = tabStore.getTabList.filter((item) => !item?.meta?.affix);
return validTabList.length == 1;
}
};
// Close right
const closeRightDisabled = index === tabStore.getTabList.length - 1 && tabStore.getLastDragEndIndex >= 0;
// update-end--author:liaozhiyang---date:20240605---for:【TV360X-732】非当前页右键关闭左侧、关闭右侧、关闭其它功能正常使用
const dropMenuList: DropMenu[] = [
{
icon: 'jam:refresh-reverse',
event: MenuEventEnum.REFRESH_PAGE,
text: t('layout.multipleTab.reload'),
disabled: refreshDisabled,
divider: true,
},
// {
// icon: 'ic:twotone-close',
// event: MenuEventEnum.CLOSE_CURRENT,
// text: t('layout.multipleTab.close'),
// disabled: !!meta?.affix || disabled,
// divider: true,
// },
{
icon: 'mdi:arrow-left',
event: MenuEventEnum.CLOSE_LEFT,
text: t('layout.multipleTab.closeLeft'),
// update-begin--author:liaozhiyang---date:20240605---for:【TV360X-732】非当前页右键关闭左侧、关闭右侧、关闭其它功能正常使用
disabled: closeLeftDisabled(),
// update-end--author:liaozhiyang---date:20240605---for:【TV360X-732】非当前页右键关闭左侧、关闭右侧、关闭其它功能正常使用
divider: false,
},
{
icon: 'mdi:arrow-right',
event: MenuEventEnum.CLOSE_RIGHT,
text: t('layout.multipleTab.closeRight'),
disabled: closeRightDisabled,
divider: true,
},
{
icon: 'material-symbols:arrows-outward',
event: MenuEventEnum.CLOSE_OTHER,
text: t('layout.multipleTab.closeOther'),
// update-begin--author:liaozhiyang---date:20240605---for:【TV360X-732】非当前页右键关闭左侧、关闭右侧、关闭其它功能正常使用
disabled: closeOtherDisabled(),
// update-end--author:liaozhiyang---date:20240605---for:【TV360X-732】非当前页右键关闭左侧、关闭右侧、关闭其它功能正常使用
},
// {
// icon: 'clarity:minus-line',
// event: MenuEventEnum.CLOSE_ALL,
// text: t('layout.multipleTab.closeAll'),
// disabled: disabled,
// },
];
return dropMenuList;
});
function handleContextMenu(tabItem: RouteLocationNormalized) {
return (e: Event) => {
if (!tabItem) {
return;
}
e?.preventDefault();
const index = tabStore.getTabList.findIndex((tab) => tab.path === tabItem.path);
state.current = tabItem;
state.currentIndex = index;
};
}
// Handle right click event
function handleMenuEvent(menu: DropMenu): void {
const { event } = menu;
switch (event) {
case MenuEventEnum.REFRESH_PAGE:
// refresh page
refreshPage();
break;
// Close current
case MenuEventEnum.CLOSE_CURRENT:
close(tabContentProps.tabItem);
break;
// Close left
case MenuEventEnum.CLOSE_LEFT:
// update-begin--author:liaozhiyang---date:20240605---for:【TV360X-732】非当前页右键关闭左侧、关闭右侧、关闭其它功能正常使用
closeLeft(state.current);
// update-end--author:liaozhiyang---date:20240605---for:【TV360X-732】非当前页右键关闭左侧、关闭右侧、关闭其它功能正常使用
break;
// Close right
case MenuEventEnum.CLOSE_RIGHT:
// update-begin--author:liaozhiyang---date:20240605---for:【TV360X-732】非当前页右键关闭左侧、关闭右侧、关闭其它功能正常使用
closeRight(state.current);
// update-end--author:liaozhiyang---date:20240605---for:【TV360X-732】非当前页右键关闭左侧、关闭右侧、关闭其它功能正常使用
break;
// Close other
case MenuEventEnum.CLOSE_OTHER:
// update-begin--author:liaozhiyang---date:20240605---for:【TV360X-732】非当前页右键关闭左侧、关闭右侧、关闭其它功能正常使用
closeOther(state.current);
// update-end--author:liaozhiyang---date:20240605---for:【TV360X-732】非当前页右键关闭左侧、关闭右侧、关闭其它功能正常使用
break;
// Close all
case MenuEventEnum.CLOSE_ALL:
// update-begin--author:liaozhiyang---date:20240605---for:【TV360X-732】非当前页右键关闭左侧、关闭右侧、关闭其它功能正常使用
closeAll(state.current);
// update-end--author:liaozhiyang---date:20240605---for:【TV360X-732】非当前页右键关闭左侧、关闭右侧、关闭其它功能正常使用
break;
}
}
return { getDropMenuList, handleMenuEvent, handleContextMenu };
}