mirror of
https://github.com/qingchencloud/clawpanel.git
synced 2026-05-29 20:30:00 +08:00
feat: Docker 集群增强 — Gateway 通讯API、像素兵种系统、互动组件、UI 优化
This commit is contained in:
151
src/components/engagement.js
Normal file
151
src/components/engagement.js
Normal file
@@ -0,0 +1,151 @@
|
||||
/**
|
||||
* 社区引导浮窗 — 适时提醒用户加群 & Star
|
||||
*
|
||||
* 触发条件(全部满足才弹出):
|
||||
* 1. 累计打开 ≥ 3 次
|
||||
* 2. 首次打开距今 ≥ 3 天
|
||||
* 3. 上次弹出距今 ≥ 14 天
|
||||
* 4. 未被永久关闭
|
||||
* 5. 由外部在"正向时机"主动调用 tryShow()
|
||||
*/
|
||||
|
||||
const KEYS = {
|
||||
firstOpen: 'clawpanel_first_open',
|
||||
openCount: 'clawpanel_open_count',
|
||||
lastShown: 'clawpanel_engage_shown',
|
||||
never: 'clawpanel_engage_never',
|
||||
}
|
||||
|
||||
const DAY = 86400000
|
||||
const MIN_OPENS = 3
|
||||
const MIN_DAYS = 3
|
||||
const COOLDOWN_DAYS = 14
|
||||
const AUTO_DISMISS_MS = 25000
|
||||
|
||||
// 启动时记录打开次数
|
||||
function _track() {
|
||||
const now = Date.now()
|
||||
if (!localStorage.getItem(KEYS.firstOpen)) {
|
||||
localStorage.setItem(KEYS.firstOpen, String(now))
|
||||
}
|
||||
const count = parseInt(localStorage.getItem(KEYS.openCount) || '0') + 1
|
||||
localStorage.setItem(KEYS.openCount, String(count))
|
||||
}
|
||||
_track()
|
||||
|
||||
function _canShow() {
|
||||
if (localStorage.getItem(KEYS.never) === '1') return false
|
||||
const count = parseInt(localStorage.getItem(KEYS.openCount) || '0')
|
||||
if (count < MIN_OPENS) return false
|
||||
const first = parseInt(localStorage.getItem(KEYS.firstOpen) || '0')
|
||||
if (Date.now() - first < MIN_DAYS * DAY) return false
|
||||
const last = parseInt(localStorage.getItem(KEYS.lastShown) || '0')
|
||||
if (Date.now() - last < COOLDOWN_DAYS * DAY) return false
|
||||
return true
|
||||
}
|
||||
|
||||
let _showing = false
|
||||
|
||||
/**
|
||||
* 在正向时机调用(如 Gateway 启动成功、配置保存成功)
|
||||
* 满足条件才弹出,否则静默返回
|
||||
*/
|
||||
export function tryShowEngagement() {
|
||||
if (_showing || !_canShow()) return
|
||||
if (document.querySelector('.engage-overlay')) return
|
||||
_showing = true
|
||||
localStorage.setItem(KEYS.lastShown, String(Date.now()))
|
||||
|
||||
const shareText = '推荐一个开源的 OpenClaw 管理面板 — ClawPanel,一键搭建、便捷管理模型和 Agent,还内置 AI 助手帮你排查问题,小白也能轻松上手 👉 https://claw.qt.cool'
|
||||
|
||||
const overlay = document.createElement('div')
|
||||
overlay.className = 'engage-overlay'
|
||||
overlay.innerHTML = `
|
||||
<div class="engage-modal">
|
||||
<button class="engage-close" title="关闭">×</button>
|
||||
|
||||
<div class="engage-header">
|
||||
<div class="engage-icon">
|
||||
<svg viewBox="0 0 24 24" width="26" height="26" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M20.84 4.61a5.5 5.5 0 00-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 00-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 000-7.78z"/></svg>
|
||||
</div>
|
||||
<div class="engage-title">感谢你使用 ClawPanel</div>
|
||||
</div>
|
||||
|
||||
<div class="engage-message">
|
||||
ClawPanel 是一个<strong>完全开源、免费</strong>的项目,由晴辰云团队专职维护、持续更新。如果它帮到了你,对我们最大的鼓励就是:
|
||||
</div>
|
||||
|
||||
<div class="engage-actions-grid">
|
||||
<a class="engage-action-card" href="https://github.com/qingchencloud/clawpanel" target="_blank" rel="noopener">
|
||||
<div class="engage-action-icon engage-action-star">
|
||||
<svg viewBox="0 0 24 24" width="22" height="22" fill="#f59e0b" stroke="#f59e0b" stroke-width="1"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>
|
||||
</div>
|
||||
<div class="engage-action-text">
|
||||
<div class="engage-action-title">GitHub Star</div>
|
||||
<div class="engage-action-desc">点个 Star 是最直接的支持</div>
|
||||
</div>
|
||||
</a>
|
||||
<div class="engage-action-card engage-action-share" data-action="copy-share">
|
||||
<div class="engage-action-icon engage-action-link">
|
||||
<svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="2"><circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><line x1="8.59" y1="13.51" x2="15.42" y2="17.49"/><line x1="15.41" y1="6.51" x2="8.59" y2="10.49"/></svg>
|
||||
</div>
|
||||
<div class="engage-action-text">
|
||||
<div class="engage-action-title">分享给朋友</div>
|
||||
<div class="engage-action-desc">复制推荐文案,让更多人知道</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="engage-section-label">扫码加入社区交流群,第一时间获取更新和帮助</div>
|
||||
<div class="engage-qrcodes">
|
||||
<a class="engage-qr-item" href="https://qt.cool/c/OpenClaw" target="_blank" rel="noopener">
|
||||
<img src="/images/OpenClaw-QQ.png" alt="QQ 交流群" />
|
||||
<div class="engage-qr-label">QQ 群</div>
|
||||
</a>
|
||||
<a class="engage-qr-item" href="https://qt.cool/c/OpenClawWx" target="_blank" rel="noopener">
|
||||
<img src="/images/OpenClawWx.png" alt="微信交流群" />
|
||||
<div class="engage-qr-label">微信群</div>
|
||||
</a>
|
||||
<a class="engage-qr-item" href="https://qt.cool/c/OpenClawDY" target="_blank" rel="noopener">
|
||||
<img src="/images/OpenClaw-DY.png" alt="抖音交流群" />
|
||||
<div class="engage-qr-label">抖音群</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="engage-footer">
|
||||
<span class="engage-never">不再提醒</span>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
document.body.appendChild(overlay)
|
||||
requestAnimationFrame(() => overlay.classList.add('engage-visible'))
|
||||
|
||||
function dismiss() {
|
||||
overlay.classList.remove('engage-visible')
|
||||
setTimeout(() => { overlay.remove(); _showing = false }, 250)
|
||||
}
|
||||
|
||||
overlay.addEventListener('click', (e) => { if (e.target === overlay) dismiss() })
|
||||
overlay.querySelector('.engage-close').onclick = dismiss
|
||||
overlay.querySelector('.engage-never').onclick = () => {
|
||||
localStorage.setItem(KEYS.never, '1')
|
||||
dismiss()
|
||||
}
|
||||
overlay.querySelector('[data-action="copy-share"]').onclick = () => {
|
||||
navigator.clipboard.writeText(shareText).then(() => {
|
||||
const desc = overlay.querySelector('[data-action="copy-share"] .engage-action-desc')
|
||||
if (desc) { desc.textContent = '✅ 已复制,去分享吧!'; setTimeout(() => { desc.textContent = '复制推荐文案,让更多人知道' }, 2000) }
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 测试用:绕过条件直接弹出(浏览器控制台输入 __testEngagement())
|
||||
window.__testEngagement = function() {
|
||||
_showing = false
|
||||
document.querySelector('.engage-overlay')?.remove()
|
||||
localStorage.removeItem(KEYS.never)
|
||||
localStorage.setItem(KEYS.openCount, '99')
|
||||
localStorage.setItem(KEYS.firstOpen, '0')
|
||||
localStorage.removeItem(KEYS.lastShown)
|
||||
tryShowEngagement()
|
||||
}
|
||||
@@ -41,9 +41,9 @@ const NAV_ITEMS_FULL = [
|
||||
]
|
||||
},
|
||||
{
|
||||
section: 'Docker',
|
||||
section: '龙虾军团',
|
||||
items: [
|
||||
{ route: '/docker', label: 'Docker 集群', icon: 'docker' },
|
||||
{ route: '/docker', label: '🦞 龙虾军团', icon: 'docker' },
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -125,6 +125,7 @@ export function renderSidebar(el) {
|
||||
<img src="/images/logo.png" alt="ClawPanel">
|
||||
</div>
|
||||
<span class="sidebar-title">ClawPanel</span>
|
||||
<button class="sidebar-close-btn" id="btn-sidebar-close" title="关闭菜单">×</button>
|
||||
</div>
|
||||
${showSwitcher ? `<div class="instance-switcher" id="instance-switcher">
|
||||
<button class="instance-current" id="btn-instance-toggle">
|
||||
@@ -186,6 +187,12 @@ export function renderSidebar(el) {
|
||||
const navItem = e.target.closest('.nav-item[data-route]')
|
||||
if (navItem) {
|
||||
navigate(navItem.dataset.route)
|
||||
_closeMobileSidebar()
|
||||
return
|
||||
}
|
||||
// 移动端关闭按钮
|
||||
if (e.target.closest('#btn-sidebar-close')) {
|
||||
_closeMobileSidebar()
|
||||
return
|
||||
}
|
||||
// 主题切换
|
||||
@@ -235,6 +242,29 @@ export function renderSidebar(el) {
|
||||
|
||||
function _escSidebar(s) { return String(s || '').replace(/</g, '<').replace(/>/g, '>') }
|
||||
|
||||
// === 移动端侧边栏 ===
|
||||
function _closeMobileSidebar() {
|
||||
const sidebar = document.getElementById('sidebar')
|
||||
const overlay = document.getElementById('sidebar-overlay')
|
||||
if (sidebar) sidebar.classList.remove('sidebar-open')
|
||||
if (overlay) overlay.classList.remove('visible')
|
||||
}
|
||||
|
||||
export function openMobileSidebar() {
|
||||
const sidebar = document.getElementById('sidebar')
|
||||
if (!sidebar) return
|
||||
sidebar.classList.add('sidebar-open')
|
||||
let overlay = document.getElementById('sidebar-overlay')
|
||||
if (!overlay) {
|
||||
overlay = document.createElement('div')
|
||||
overlay.id = 'sidebar-overlay'
|
||||
overlay.className = 'sidebar-overlay'
|
||||
overlay.addEventListener('click', _closeMobileSidebar)
|
||||
document.getElementById('app').appendChild(overlay)
|
||||
}
|
||||
requestAnimationFrame(() => overlay.classList.add('visible'))
|
||||
}
|
||||
|
||||
function _closeInstanceDropdown() {
|
||||
const dd = document.getElementById('instance-dropdown')
|
||||
if (dd) dd.classList.remove('open')
|
||||
|
||||
Reference in New Issue
Block a user