349 lines
12 KiB
JavaScript
349 lines
12 KiB
JavaScript
|
class TabsContainer extends HTMLElement {
|
|||
|
constructor() {
|
|||
|
super();
|
|||
|
this.attachShadow({ mode: 'open' });
|
|||
|
this.openTabs = new Set();
|
|||
|
this.currentActiveTabId = null; // 跟踪当前激活的标签页ID
|
|||
|
}
|
|||
|
|
|||
|
connectedCallback() {
|
|||
|
this.render();
|
|||
|
this.setupEventListeners();
|
|||
|
}
|
|||
|
|
|||
|
render() {
|
|||
|
this.shadowRoot.innerHTML = `
|
|||
|
<style>
|
|||
|
:host {
|
|||
|
display: block;
|
|||
|
}
|
|||
|
|
|||
|
.tabs-container {
|
|||
|
background-color: white;
|
|||
|
border-bottom: 1px solid #e0e0e0;
|
|||
|
position: relative;
|
|||
|
display: flex;
|
|||
|
align-items: center;
|
|||
|
padding: 0 4px;
|
|||
|
height: 41px;
|
|||
|
box-sizing: border-box;
|
|||
|
}
|
|||
|
|
|||
|
.tabs-header {
|
|||
|
display: flex;
|
|||
|
padding: 0 32px;
|
|||
|
overflow-x: auto;
|
|||
|
overflow-y: hidden;
|
|||
|
height: 40px;
|
|||
|
flex: 1;
|
|||
|
scroll-behavior: smooth;
|
|||
|
scrollbar-width: none;
|
|||
|
-ms-overflow-style: none;
|
|||
|
}
|
|||
|
|
|||
|
.tabs-header::-webkit-scrollbar {
|
|||
|
display: none;
|
|||
|
}
|
|||
|
|
|||
|
.tab {
|
|||
|
display: flex;
|
|||
|
align-items: center;
|
|||
|
padding: 0 16px;
|
|||
|
height: 100%;
|
|||
|
background-color: #f5f5f5;
|
|||
|
border: 1px solid #e0e0e0;
|
|||
|
border-bottom: none;
|
|||
|
cursor: pointer;
|
|||
|
transition: all 0.2s ease;
|
|||
|
white-space: nowrap;
|
|||
|
flex: 0 0 auto;
|
|||
|
gap: 8px;
|
|||
|
margin-right: -1px;
|
|||
|
margin-top: 4px;
|
|||
|
position: relative;
|
|||
|
border-radius: 4px 4px 0 0;
|
|||
|
}
|
|||
|
|
|||
|
.tab:hover {
|
|||
|
background-color: #fafafa;
|
|||
|
}
|
|||
|
|
|||
|
.tab span {
|
|||
|
margin: 0;
|
|||
|
overflow: hidden;
|
|||
|
text-overflow: ellipsis;
|
|||
|
white-space: nowrap;
|
|||
|
font-size: 15px;
|
|||
|
color: #666;
|
|||
|
line-height: 1.2;
|
|||
|
display: inline-block;
|
|||
|
max-width: 200px;
|
|||
|
}
|
|||
|
|
|||
|
.tab.active {
|
|||
|
background-color: white;
|
|||
|
border-bottom: none;
|
|||
|
margin-top: 0;
|
|||
|
height: calc(100% + 1px);
|
|||
|
z-index: 1;
|
|||
|
box-shadow: 0 -2px 5px rgba(0,0,0,0.05);
|
|||
|
}
|
|||
|
|
|||
|
.tab.active span {
|
|||
|
color: #8B6DB3;
|
|||
|
font-weight: 800;
|
|||
|
}
|
|||
|
|
|||
|
.tab.active::after {
|
|||
|
content: '';
|
|||
|
position: absolute;
|
|||
|
bottom: 0;
|
|||
|
left: 0;
|
|||
|
right: 0;
|
|||
|
height: 2px;
|
|||
|
background-color: #1890ff;
|
|||
|
}
|
|||
|
|
|||
|
.icon-small {
|
|||
|
width: 18px;
|
|||
|
height: 18px;
|
|||
|
flex-shrink: 0;
|
|||
|
}
|
|||
|
|
|||
|
.close-tab {
|
|||
|
display: flex;
|
|||
|
align-items: center;
|
|||
|
justify-content: center;
|
|||
|
width: 16px;
|
|||
|
height: 16px;
|
|||
|
opacity: 0.5;
|
|||
|
transition: opacity 0.3s;
|
|||
|
margin-left: 4px;
|
|||
|
flex-shrink: 0;
|
|||
|
}
|
|||
|
|
|||
|
.close-tab:hover {
|
|||
|
opacity: 1;
|
|||
|
}
|
|||
|
|
|||
|
.close-tab img {
|
|||
|
width: 12px;
|
|||
|
height: 12px;
|
|||
|
}
|
|||
|
|
|||
|
.tabs-scroll-button {
|
|||
|
width: 28px;
|
|||
|
height: 28px;
|
|||
|
background-color: #7986E7;
|
|||
|
border-radius: 50%;
|
|||
|
display: none;
|
|||
|
justify-content: center;
|
|||
|
align-items: center;
|
|||
|
cursor: pointer;
|
|||
|
position: absolute;
|
|||
|
top: 50%;
|
|||
|
transform: translateY(-50%);
|
|||
|
z-index: 10;
|
|||
|
transition: all 0.3s ease;
|
|||
|
border: none;
|
|||
|
opacity: 0;
|
|||
|
pointer-events: none;
|
|||
|
}
|
|||
|
|
|||
|
.tabs-scroll-button:hover {
|
|||
|
background-color: rgba(0,0,0,0.08);
|
|||
|
}
|
|||
|
|
|||
|
.tabs-scroll-button.left {
|
|||
|
left: 4px;
|
|||
|
}
|
|||
|
|
|||
|
.tabs-scroll-button.right {
|
|||
|
right: 4px;
|
|||
|
}
|
|||
|
|
|||
|
.tabs-scroll-button.show {
|
|||
|
display: flex;
|
|||
|
opacity: 1;
|
|||
|
pointer-events: auto;
|
|||
|
}
|
|||
|
</style>
|
|||
|
<div class="tabs-container">
|
|||
|
<div class="tabs-scroll-button left" id="scrollLeft">
|
|||
|
<img src="assets/icons/png/chevron-dleft_b.png" alt="向左滚动" class="icon-small">
|
|||
|
</div>
|
|||
|
<div class="tabs-header"></div>
|
|||
|
<div class="tabs-scroll-button right" id="scrollRight">
|
|||
|
<img src="assets/icons/png/chevron-dright_b.png" alt="向右滚动" class="icon-small">
|
|||
|
</div>
|
|||
|
</div>
|
|||
|
`;
|
|||
|
}
|
|||
|
|
|||
|
setupEventListeners() {
|
|||
|
const tabsHeader = this.shadowRoot.querySelector('.tabs-header');
|
|||
|
const scrollLeftBtn = this.shadowRoot.getElementById('scrollLeft');
|
|||
|
const scrollRightBtn = this.shadowRoot.getElementById('scrollRight');
|
|||
|
|
|||
|
// 检查是否需要显示滚动按钮
|
|||
|
const checkScrollButtons = () => {
|
|||
|
const { scrollLeft, scrollWidth, clientWidth } = tabsHeader;
|
|||
|
const hasScroll = scrollWidth > clientWidth;
|
|||
|
|
|||
|
scrollLeftBtn.classList.toggle('show', hasScroll && scrollLeft > 0);
|
|||
|
scrollRightBtn.classList.toggle('show', hasScroll && scrollLeft < scrollWidth - clientWidth - 1);
|
|||
|
};
|
|||
|
|
|||
|
// 添加滚动事件监听
|
|||
|
tabsHeader.addEventListener('scroll', checkScrollButtons);
|
|||
|
window.addEventListener('resize', checkScrollButtons);
|
|||
|
|
|||
|
// 滚动按钮点击事件
|
|||
|
scrollLeftBtn.addEventListener('click', () => {
|
|||
|
tabsHeader.scrollBy({ left: -200, behavior: 'smooth' });
|
|||
|
});
|
|||
|
|
|||
|
scrollRightBtn.addEventListener('click', () => {
|
|||
|
tabsHeader.scrollBy({ left: 200, behavior: 'smooth' });
|
|||
|
});
|
|||
|
|
|||
|
// 添加 MutationObserver 监听标签页的变化
|
|||
|
const observer = new MutationObserver(checkScrollButtons);
|
|||
|
observer.observe(tabsHeader, {
|
|||
|
childList: true,
|
|||
|
subtree: true,
|
|||
|
attributes: true,
|
|||
|
attributeFilter: ['class']
|
|||
|
});
|
|||
|
|
|||
|
// 初始检查滚动按钮状态
|
|||
|
checkScrollButtons();
|
|||
|
}
|
|||
|
|
|||
|
activateTab(id) {
|
|||
|
const activeTab = this.shadowRoot.querySelector(`.tab[data-tab="${id}"]`);
|
|||
|
if (!activeTab) return;
|
|||
|
|
|||
|
// 检查是否是当前已激活的标签页,如果是则不触发事件
|
|||
|
if (this.currentActiveTabId === id) {
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
// 记录新的激活标签页ID
|
|||
|
const previousTabId = this.currentActiveTabId;
|
|||
|
this.currentActiveTabId = id;
|
|||
|
|
|||
|
this.shadowRoot.querySelectorAll('.tab').forEach(tab => {
|
|||
|
tab.classList.toggle('active', tab.getAttribute('data-tab') === id);
|
|||
|
});
|
|||
|
|
|||
|
const title = activeTab.getAttribute('data-title') || activeTab.querySelector('span').textContent;
|
|||
|
const parentText = activeTab.getAttribute('data-parent-text') || '主页';
|
|||
|
const parentTool = activeTab.getAttribute('data-parent-tool');
|
|||
|
|
|||
|
// 触发标签页激活事件,并传递前一个标签页ID
|
|||
|
this.dispatchEvent(new CustomEvent('tab-activated', {
|
|||
|
detail: {
|
|||
|
id,
|
|||
|
title,
|
|||
|
parentText,
|
|||
|
parentTool,
|
|||
|
previousTabId, // 添加前一个标签页ID
|
|||
|
isTabSwitch: true // 标记为真正的标签切换
|
|||
|
}
|
|||
|
}));
|
|||
|
|
|||
|
// 同步左侧子工具栏选中状态
|
|||
|
if (parentTool) {
|
|||
|
// 先选中主工具栏
|
|||
|
const mainToolbar = document.querySelector('main-toolbar');
|
|||
|
const mainToolItem = mainToolbar.shadowRoot.querySelector(`.tool-item[data-tool="${parentTool}"]`);
|
|||
|
if (mainToolItem) {
|
|||
|
// 移除所有工具项的激活状态
|
|||
|
mainToolbar.shadowRoot.querySelectorAll('.tool-item').forEach(t => t.classList.remove('active'));
|
|||
|
// 激活当前工具项
|
|||
|
mainToolItem.classList.add('active');
|
|||
|
}
|
|||
|
|
|||
|
// 再同步子工具栏
|
|||
|
const subToolbar = document.querySelector('sub-toolbar');
|
|||
|
subToolbar.setAttribute('current-tool', parentTool);
|
|||
|
|
|||
|
// 找到并点击对应的子菜单项
|
|||
|
setTimeout(() => {
|
|||
|
const subMenuItems = subToolbar.shadowRoot.querySelectorAll('.sub-item');
|
|||
|
const targetSubItem = Array.from(subMenuItems).find(item =>
|
|||
|
item.textContent.trim() === title &&
|
|||
|
item.closest('.sub-menu').getAttribute('data-parent') === parentTool
|
|||
|
);
|
|||
|
|
|||
|
if (targetSubItem) {
|
|||
|
// 移除所有子菜单项的激活状态
|
|||
|
subMenuItems.forEach(item => item.classList.remove('active'));
|
|||
|
// 激活当前子菜单项
|
|||
|
targetSubItem.classList.add('active');
|
|||
|
}
|
|||
|
}, 0); // 使用setTimeout确保DOM已更新
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
createTab(id, title, icon, parentText, parentTool) {
|
|||
|
// 对于其他标签页,如果已经打开则激活
|
|||
|
if (this.openTabs.has(id)) {
|
|||
|
this.activateTab(id);
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
const tab = document.createElement('div');
|
|||
|
tab.className = 'tab';
|
|||
|
tab.setAttribute('data-tab', id);
|
|||
|
tab.setAttribute('data-parent-text', parentText || '主页');
|
|||
|
tab.setAttribute('data-parent-tool', parentTool || 'home');
|
|||
|
tab.setAttribute('data-title', title);
|
|||
|
tab.innerHTML = `
|
|||
|
<img src="assets/icons/png/${icon}_b.png" alt="${title}" class="icon-small">
|
|||
|
<span>${title}</span>
|
|||
|
${id !== 'overview' ? '<div class="close-tab"><img src="assets/icons/png/close_b.png" alt="关闭"></div>' : ''}
|
|||
|
`;
|
|||
|
|
|||
|
this.shadowRoot.querySelector('.tabs-header').appendChild(tab);
|
|||
|
|
|||
|
tab.addEventListener('click', () => {
|
|||
|
this.activateTab(id);
|
|||
|
});
|
|||
|
|
|||
|
if (id !== 'overview') {
|
|||
|
const closeBtn = tab.querySelector('.close-tab');
|
|||
|
closeBtn.addEventListener('click', (e) => {
|
|||
|
e.stopPropagation();
|
|||
|
this.closeTab(id);
|
|||
|
});
|
|||
|
}
|
|||
|
|
|||
|
this.openTabs.add(id);
|
|||
|
this.activateTab(id);
|
|||
|
}
|
|||
|
|
|||
|
closeTab(id) {
|
|||
|
if (id === 'overview') return;
|
|||
|
|
|||
|
const tab = this.shadowRoot.querySelector(`.tab[data-tab="${id}"]`);
|
|||
|
|
|||
|
if (tab.classList.contains('active')) {
|
|||
|
const prevTab = tab.previousElementSibling || tab.nextElementSibling;
|
|||
|
if (prevTab) {
|
|||
|
this.activateTab(prevTab.getAttribute('data-tab'));
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
tab.remove();
|
|||
|
this.openTabs.delete(id);
|
|||
|
|
|||
|
// 触发标签页关闭事件
|
|||
|
this.dispatchEvent(new CustomEvent('tab-closed', {
|
|||
|
detail: { id }
|
|||
|
}));
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
customElements.define('tabs-container', TabsContainer);
|