菜单系统
位于/theme-hao/templates/modules/layouts/layout.html文件下
以
<!-- 手机端 按钮 -->
<div th:replace="~{modules/widgets/floating_buttons}"></div>
引用
源码位于
/themes/theme-hao/templates/modules/widgets/floating_buttons.html文件
<script>
class FloatingButtons extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
shadow.innerHTML = `
<style>
/* 基础样式 */
:host {
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: 900;
display: none;
box-sizing: border-box;
}
/* 在小于768px的屏幕上显示 */
@media (max-width: 768px) {
:host {
display: block;
}
}
/* 在archives路径下隐藏组件 */
:host([hidden-by-path]) {
display: none !important;
}
.container {
display: flex;
justify-content: center;
padding: 0.5rem;
padding: clamp(0.5rem, 3vw, 0.75rem);
}
.button-group {
background: white;
border-radius: 9999px;
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.07);
display: flex;
padding: 0.25rem;
padding: clamp(0.25rem, 1vw, 0.375rem);
width: 85%;
max-width: 280px;
justify-content: space-around;
}
.button {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 0.375rem;
padding: clamp(0.375rem, 1.5vw, 0.5rem);
border-radius: 50%;
transition: all 0.2s ease;
background: none;
border: none;
cursor: pointer;
outline: none;
-webkit-tap-highlight-color: transparent;
}
.button:hover,
.button:active {
transform: translateY(-1px);
}
.button-icon {
width: 1.75rem;
width: clamp(1.75rem, 6vw, 2.25rem);
height: 1.75rem;
height: clamp(1.75rem, 6vw, 2.25rem);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 0.0625rem;
margin-bottom: clamp(0.0625rem, 0.5vw, 0.125rem);
}
.button-icon svg {
width: 0.875rem;
width: clamp(0.875rem, 3vw, 1rem);
height: 0.875rem;
height: clamp(0.875rem, 3vw, 1rem);
}
.button-text {
font-size: 0.4375rem;
font-size: clamp(0.4375rem, 1.5vw, 0.5625rem);
font-weight: 500;
letter-spacing: 0.025rem;
}
/* 按钮颜色 */
.home .button-icon { background-color: rgba(59, 130, 246, 0.1); }
.home .button-text { color: #3b82f6; }
.message .button-icon { background-color: rgba(139, 92, 246, 0.1); }
.message .button-text { color: #8b5cf6; }
.search .button-icon { background-color: rgba(16, 185, 129, 0.1); }
.search .button-text { color: #10b981; }
.profile .button-icon { background-color: rgba(249, 115, 22, 0.1); }
.profile .button-text { color: #f97316; }
/* 中间大按钮 */
.center-button {
width: 2.5rem;
width: clamp(2.5rem, 9vw, 3rem);
height: 2.5rem;
height: clamp(2.5rem, 9vw, 3rem);
border-radius: 50%;
background: linear-gradient(135deg, #3b82f6, #6d28d9);
box-shadow: 0 6px 16px rgba(93, 52, 236, 0.2);
display: flex;
align-items: center;
justify-content: center;
position: relative;
margin-top: -0.5rem;
margin-top: clamp(-0.5rem, -2vw, -0.75rem);
transition: all 0.2s ease;
}
.center-button svg {
width: 1rem;
width: clamp(1rem, 4vw, 1.25rem);
height: 1rem;
height: clamp(1rem, 4vw, 1.25rem);
}
.center-button:hover,
.center-button:active {
transform: scale(1.03);
}
/* 脉冲效果 */
.pulse {
position: absolute;
width: 100%;
height: 100%;
border-radius: 50%;
background-color: rgba(255, 255, 255, 0.3);
animation: pulse-animation 2s infinite;
}
@keyframes pulse-animation {
0% { transform: scale(1); opacity: 1; }
100% { transform: scale(1.6); opacity: 0; }
}
</style>
<div class="container">
<div class="button-group">
<button class="button home" aria-label="首页">
<div class="button-icon">
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9 22H15C20 22 22 20 22 15V9C22 4 20 2 15 2H9C4 2 2 4 2 9V15C2 20 4 22 9 22Z" stroke="#3b82f6" stroke-width="2"/>
<path d="M2 9L12 2L22 9" stroke="#3b82f6" stroke-width="2"/>
</svg>
</div>
<span class="button-text">首页</span>
</button>
<button class="button message" aria-label="消息">
<div class="button-icon">
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4 4H20C21.1 4 22 4.9 22 6V18C22 19.1 21.1 20 20 20H4C2.9 20 2 19.1 2 18V6C2 4.9 2.9 4 4 4Z" stroke="#8b5cf6" stroke-width="2"/>
<path d="M2 6L12 13L22 6" stroke="#8b5cf6" stroke-width="2"/>
</svg>
</div>
<span class="button-text">消息</span>
</button>
<button class="button center-button" aria-label="添加">
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 5V19" stroke="white" stroke-width="2" stroke-linecap="round"/>
<path d="M5 12H19" stroke="white" stroke-width="2" stroke-linecap="round"/>
</svg>
<div class="pulse"></div>
</button>
<button class="button search" aria-label="搜索">
<div class="button-icon">
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="11" cy="11" r="7" stroke="#10b981" stroke-width="2"/>
<path d="M16 16L20 20" stroke="#10b981" stroke-width="2"/>
</svg>
</div>
<span class="button-text">搜索</span>
</button>
<button class="button profile" aria-label="我的">
<div class="button-icon">
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="8" r="5" stroke="#f97316" stroke-width="2"/>
<path d="M3 22C3 18 7 17 12 17C17 17 21 18 21 22" stroke="#f97316" stroke-width="2"/>
</svg>
</div>
<span class="button-text">我的</span>
</button>
</div>
</div>
`;
// 设置事件监听
this.setupEventListeners();
// 初始化时检查当前路径
this.checkCurrentPath();
}
// 获取需要隐藏的路径列表
getHiddenPaths() {
// 从组件属性获取隐藏路径列表,或使用默认值
const hiddenPathsAttr = this.getAttribute('hidden-paths');
if (hiddenPathsAttr) {
return hiddenPathsAttr.split(',').map(path => path.trim());
}
// 默认隐藏的路径
return [
'/archives/',
'/bangumis',
'/liu-yan-ban',
'/yin-le'
];
}
// 检查当前路径并设置隐藏属性
checkCurrentPath() {
const path = window.location.pathname;
const hiddenPaths = this.getHiddenPaths();
// 检查当前路径是否匹配任何需要隐藏的路径
const shouldHide = hiddenPaths.some(hiddenPath => {
// 如果隐藏路径以/开头但不以/结尾,视为精确匹配
if (hiddenPath.startsWith('/') && !hiddenPath.endsWith('/')) {
return path === hiddenPath;
}
// 否则视为前缀匹配
return path.startsWith(hiddenPath);
});
if (shouldHide) {
this.setAttribute('hidden-by-path', '');
} else {
this.removeAttribute('hidden-by-path');
}
}
// 设置按钮点击事件
setupEventListeners() {
const shadow = this.shadowRoot;
shadow.querySelectorAll('.button').forEach(button => {
button.addEventListener('click', (e) => {
e.preventDefault();
const action = button.getAttribute('aria-label');
// 特殊处理搜索按钮
if (action === '搜索') {
this.openSearchWidget();
return;
}
// 处理其他按钮链接
const link = this.getButtonLink(action);
if (link) {
window.location.href = link;
} else {
this.dispatchEvent(new CustomEvent('button-click', {
detail: { action },
bubbles: true,
composed: true
}));
}
});
});
}
// 打开搜索插件
openSearchWidget() {
// 检查搜索插件是否可用
if (window.SearchWidget && typeof SearchWidget.open === 'function') {
console.log('调用搜索插件...');
SearchWidget.open(); // 调用现有搜索插件
} else {
console.error('搜索插件不可用,使用备用链接');
// 备用逻辑:跳转到搜索页面
const fallbackLink = this.getAttribute('search-link') || '/search';
window.location.href = fallbackLink;
}
}
// 获取按钮对应的链接
getButtonLink(action) {
const linkMap = {
'首页': this.getAttribute('home-link'),
'消息': this.getAttribute('message-link'),
'添加': this.getAttribute('add-link'),
'我的': this.getAttribute('profile-link')
};
return linkMap[action] || null;
}
// 元素连接到DOM时触发
connectedCallback() {
console.log('Floating buttons connected');
// 监听路由变化
this.setupRouteListeners();
}
// 设置路由变化监听
setupRouteListeners() {
// 监听原生popstate事件(后退/前进按钮)
window.addEventListener('popstate', () => this.checkCurrentPath());
// 监听pushState和replaceState方法
const self = this;
// 保存原始方法
const originalPushState = history.pushState;
const originalReplaceState = history.replaceState;
// 重写pushState方法
history.pushState = function(state, title, url) {
originalPushState.apply(this, arguments);
self.checkCurrentPath();
};
// 重写replaceState方法
history.replaceState = function(state, title, url) {
originalReplaceState.apply(this, arguments);
self.checkCurrentPath();
};
// 监听hashchange事件(处理#后面的变化)
window.addEventListener('hashchange', () => this.checkCurrentPath());
}
// 元素从DOM移除时触发
disconnectedCallback() {
const shadow = this.shadowRoot;
shadow.querySelectorAll('.button').forEach(button => {
button.removeEventListener('click', null);
});
// 移除路由监听
window.removeEventListener('popstate', () => this.checkCurrentPath());
window.removeEventListener('hashchange', () => this.checkCurrentPath());
// 注意:无法恢复原始的pushState和replaceState方法,因为没有引用
}
}
// 在DOMContentLoaded后定义自定义元素
document.addEventListener('DOMContentLoaded', () => {
if (!customElements.get('floating-buttons')) {
customElements.define('floating-buttons', FloatingButtons);
}
});
</script>
<!-- 使用示例 - 自定义隐藏路径 -->
<floating-buttons
home-link="/#"
message-link="/uc/notifications"
add-link="/uc/posts/editor"
search-link="/search"
profile-link="/uc/profile"
hidden-paiew="/archives/,/bangumis,/liu-yan-ban,/yin-le,/new-page,/1732350800610,/privacy,/1731941877494">
</floating-buttons>
其中hidden-paiew为添加更多隐藏菜单的路由,需要注意的的目前类似于/guan-yu-feng-jun-xiao-zhan这样的不生效