mirror of
https://github.com/qingchencloud/clawpanel.git
synced 2026-06-28 03:01:54 +08:00
feat(update): integrate official site update flow
This commit is contained in:
@@ -234,6 +234,7 @@ export function renderSidebar(el) {
|
||||
const isDark = getTheme() === 'dark'
|
||||
const sunIcon = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>'
|
||||
const moonIcon = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12.79A9 9 0 1111.21 3 7 7 0 0021 12.79z"/></svg>'
|
||||
const bellIcon = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 8a6 6 0 10-12 0c0 7-3 7-3 9h18c0-2-3-2-3-9"/><path d="M13.73 21a2 2 0 01-3.46 0"/></svg>'
|
||||
|
||||
const langCode = getLang()
|
||||
const langs = getAvailableLangs()
|
||||
@@ -254,19 +255,22 @@ export function renderSidebar(el) {
|
||||
|
||||
html += `
|
||||
<div class="sidebar-footer">
|
||||
<div class="nav-item" id="btn-theme-toggle">
|
||||
${isDark ? sunIcon : moonIcon}
|
||||
<span>${isDark ? t('sidebar.themeLight') : t('sidebar.themeDark')}</span>
|
||||
</div>
|
||||
<div class="lang-switcher" id="lang-switcher">
|
||||
<button class="nav-item lang-trigger" id="btn-lang-toggle">
|
||||
${globeIcon}
|
||||
<span>${currentLang.label}</span>
|
||||
<svg class="lang-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="12" height="12"><path d="M18 15l-6-6-6 6"/></svg>
|
||||
<div class="sidebar-tools" aria-label="ClawPanel tools">
|
||||
<button class="sidebar-tool-btn site-message-trigger" type="button" title="${t('siteMessages.title')}" aria-label="${t('siteMessages.title')}">
|
||||
${bellIcon}
|
||||
<span class="site-message-tool-badge" aria-hidden="true"></span>
|
||||
</button>
|
||||
<div class="lang-dropdown" id="lang-dropdown">
|
||||
${langs.length > 4 ? '<div class="lang-search-wrap"><input class="lang-search" id="lang-search" type="text" placeholder="Search..." autocomplete="off"></div>' : ''}
|
||||
<div class="lang-options" id="lang-options">${langOptions}</div>
|
||||
<button class="sidebar-tool-btn" id="btn-theme-toggle" type="button" title="${isDark ? t('sidebar.themeLight') : t('sidebar.themeDark')}" aria-label="${isDark ? t('sidebar.themeLight') : t('sidebar.themeDark')}">
|
||||
${isDark ? sunIcon : moonIcon}
|
||||
</button>
|
||||
<div class="lang-switcher" id="lang-switcher">
|
||||
<button class="sidebar-tool-btn lang-trigger" id="btn-lang-toggle" type="button" title="${currentLang.label}" aria-label="${currentLang.label}">
|
||||
${globeIcon}
|
||||
</button>
|
||||
<div class="lang-dropdown" id="lang-dropdown">
|
||||
${langs.length > 4 ? '<div class="lang-search-wrap"><input class="lang-search" id="lang-search" type="text" placeholder="Search..." autocomplete="off"></div>' : ''}
|
||||
<div class="lang-options" id="lang-options">${langOptions}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sidebar-meta">
|
||||
@@ -277,6 +281,7 @@ export function renderSidebar(el) {
|
||||
`
|
||||
|
||||
el.innerHTML = html
|
||||
window.dispatchEvent(new CustomEvent('clawpanel:site-message-launcher-mounted'))
|
||||
|
||||
// 应用折叠态(桌面端)
|
||||
_setDesktopSidebarCollapsed(collapsed)
|
||||
@@ -513,4 +518,3 @@ function _filterLangOptions(query) {
|
||||
opt.style.display = (label.includes(q) || code.includes(q)) ? '' : 'none'
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
420
src/components/site-message-center.js
Normal file
420
src/components/site-message-center.js
Normal file
@@ -0,0 +1,420 @@
|
||||
import { t } from '../lib/i18n.js'
|
||||
|
||||
const ICON_BELL = '<svg viewBox="0 0 24 24"><path d="M18 8a6 6 0 10-12 0c0 7-3 7-3 9h18c0-2-3-2-3-9"/><path d="M13.73 21a2 2 0 01-3.46 0"/></svg>'
|
||||
const ICON_SEND = '<svg viewBox="0 0 24 24"><path d="M22 2L11 13"/><path d="M22 2l-7 20-4-9-9-4 20-7z"/></svg>'
|
||||
const ICON_X = '<svg viewBox="0 0 24 24"><path d="M18 6L6 18"/><path d="M6 6l12 12"/></svg>'
|
||||
const DISMISS_PREFIX = 'clawpanel_announcement_dismissed_'
|
||||
const TODAY_CLOSE_KEY = 'clawpanel_site_message_closed_today'
|
||||
const LAUNCHER_SELECTOR = '.site-message-trigger'
|
||||
|
||||
let _fetcher = null
|
||||
let _overlay = null
|
||||
let _messages = { notifications: [], announcements: [] }
|
||||
let _activeTab = 'notifications'
|
||||
let _launcherBound = false
|
||||
|
||||
export function initSiteMessageCenter({ fetcher } = {}) {
|
||||
_fetcher = typeof fetcher === 'function' ? fetcher : null
|
||||
bindLaunchers()
|
||||
updateLauncherBadge()
|
||||
|
||||
window.addEventListener('clawpanel:site-message-launcher-mounted', updateLauncherBadge)
|
||||
window.addEventListener('clawpanel:show-site-messages', async (event) => {
|
||||
const detail = event.detail || {}
|
||||
const payload = detail.payload || (detail.notifications || detail.announcements ? detail : null)
|
||||
if (payload && typeof payload === 'object' && (payload.notifications || payload.announcements)) {
|
||||
setSiteMessageCenterPayload(payload)
|
||||
openSiteMessageCenter({ force: true, tab: detail.tab })
|
||||
return
|
||||
}
|
||||
if (detail.tab) {
|
||||
openSiteMessageCenter({ force: true, tab: detail.tab })
|
||||
return
|
||||
}
|
||||
await refreshSiteMessageCenter({ auto: false, forceOpen: true })
|
||||
})
|
||||
}
|
||||
|
||||
export async function refreshSiteMessageCenter({ auto = false, forceOpen = false, tab = null } = {}) {
|
||||
if (!_fetcher) return
|
||||
try {
|
||||
const payload = await _fetcher()
|
||||
setSiteMessageCenterPayload(payload)
|
||||
if (forceOpen) {
|
||||
openSiteMessageCenter({ force: true, tab })
|
||||
} else if (auto && shouldAutoOpen()) {
|
||||
openSiteMessageCenter({ tab })
|
||||
}
|
||||
} catch {
|
||||
if (forceOpen) openSiteMessageCenter({ force: true, tab })
|
||||
}
|
||||
}
|
||||
|
||||
export function setSiteMessageCenterPayload(payload) {
|
||||
_messages = normalizePayload(payload)
|
||||
if (!_messages[_activeTab]?.length) {
|
||||
_activeTab = _messages.notifications.length ? 'notifications' : 'announcements'
|
||||
}
|
||||
updateLauncherBadge()
|
||||
}
|
||||
|
||||
export function normalizeSiteMessagePayload(payload = {}) {
|
||||
return normalizePayload(payload)
|
||||
}
|
||||
|
||||
export function openSiteMessageCenter({ force = false, tab = null } = {}) {
|
||||
const visible = getVisibleMessages()
|
||||
if (!force && !visible.notifications.length && !visible.announcements.length) return
|
||||
_activeTab = tab || (visible.notifications.length ? 'notifications' : 'announcements')
|
||||
renderModal()
|
||||
}
|
||||
|
||||
function bindLaunchers() {
|
||||
if (_launcherBound) return
|
||||
_launcherBound = true
|
||||
document.addEventListener('click', (event) => {
|
||||
const btn = event.target.closest(LAUNCHER_SELECTOR)
|
||||
if (!btn) return
|
||||
event.preventDefault()
|
||||
refreshSiteMessageCenter({ forceOpen: true }).catch(() => openSiteMessageCenter({ force: true }))
|
||||
})
|
||||
}
|
||||
|
||||
function updateLauncherBadge() {
|
||||
const count = getVisibleMessages().notifications.length + getVisibleMessages().announcements.length
|
||||
document.querySelectorAll(LAUNCHER_SELECTOR).forEach((launcher) => {
|
||||
launcher.classList.toggle('has-unread', count > 0)
|
||||
const badge = launcher.querySelector('.site-message-tool-badge, .site-message-fab-badge')
|
||||
if (badge) badge.textContent = count > 9 ? '9+' : String(count || '')
|
||||
})
|
||||
}
|
||||
|
||||
function renderModal() {
|
||||
const old = document.getElementById('site-message-overlay')
|
||||
if (old) old.remove()
|
||||
|
||||
const visible = getVisibleMessages()
|
||||
|
||||
const overlay = document.createElement('div')
|
||||
overlay.id = 'site-message-overlay'
|
||||
overlay.className = 'site-message-overlay'
|
||||
overlay.setAttribute('role', 'dialog')
|
||||
overlay.setAttribute('aria-modal', 'true')
|
||||
overlay.innerHTML = `
|
||||
<section class="site-message-modal" tabindex="-1">
|
||||
<header class="site-message-header">
|
||||
<div class="site-message-title">
|
||||
<span class="site-message-title-icon" aria-hidden="true">${ICON_BELL}</span>
|
||||
<div>
|
||||
<h2>${t('siteMessages.title')}</h2>
|
||||
<p>${formatSummary(visible)}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="site-message-tabs" role="tablist">
|
||||
${renderTab('notifications', t('siteMessages.notifications'), ICON_BELL, visible.notifications.length)}
|
||||
${renderTab('announcements', t('siteMessages.announcements'), ICON_SEND, visible.announcements.length)}
|
||||
</div>
|
||||
<button class="site-message-close" type="button" title="${t('common.close')}">${ICON_X}</button>
|
||||
</header>
|
||||
<div class="site-message-body">
|
||||
${_activeTab === 'notifications' ? renderNotifications(visible.notifications) : renderAnnouncements(visible.announcements)}
|
||||
</div>
|
||||
<footer class="site-message-footer">
|
||||
<button class="btn btn-secondary btn-sm" type="button" data-site-message-today>${t('siteMessages.closeToday')}</button>
|
||||
<button class="btn btn-primary btn-sm" type="button" data-site-message-dismiss>${t('siteMessages.closeCurrent')}</button>
|
||||
</footer>
|
||||
</section>
|
||||
`
|
||||
|
||||
document.body.appendChild(overlay)
|
||||
_overlay = overlay
|
||||
bindModalEvents(overlay)
|
||||
const modal = overlay.querySelector('.site-message-modal')
|
||||
modal?.focus({ preventScroll: true })
|
||||
}
|
||||
|
||||
function renderTab(tab, label, icon, count) {
|
||||
const active = _activeTab === tab
|
||||
return `
|
||||
<button class="site-message-tab ${active ? 'active' : ''}" type="button" role="tab" aria-selected="${active}" data-site-message-tab="${tab}">
|
||||
${icon}<span>${label}</span>${count ? `<small>${count}</small>` : ''}
|
||||
</button>
|
||||
`
|
||||
}
|
||||
|
||||
function renderNotifications(items) {
|
||||
if (!items.length) {
|
||||
return `
|
||||
<div class="site-message-empty">
|
||||
<span class="site-message-empty-icon" aria-hidden="true">${ICON_BELL}</span>
|
||||
<strong>${t('siteMessages.emptyNotifications')}</strong>
|
||||
<p>${t('siteMessages.emptyNotificationsHint')}</p>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
return `
|
||||
<div class="site-message-section-head">
|
||||
<span>${t('siteMessages.notificationFeed')}</span>
|
||||
<small>${t('siteMessages.sortedByTime')}</small>
|
||||
</div>
|
||||
<div class="site-message-timeline">
|
||||
${items.map(item => `
|
||||
<article class="site-message-timeline-item level-${escapeAttr(item.level)}">
|
||||
<div class="site-message-dot" aria-hidden="true"></div>
|
||||
<div class="site-message-line" aria-hidden="true"></div>
|
||||
<div class="site-message-timeline-content">
|
||||
<div class="site-message-item-top">
|
||||
<h3>${escapeHtml(item.title)}</h3>
|
||||
<span>${renderLevelLabel(item.level)}</span>
|
||||
</div>
|
||||
${item.body ? `<p>${escapeHtml(item.body)}</p>` : ''}
|
||||
<div class="site-message-meta">${formatMessageTime(item)}</div>
|
||||
${renderMessageCta(item)}
|
||||
</div>
|
||||
</article>
|
||||
`).join('')}
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
function renderAnnouncements(items) {
|
||||
if (!items.length) {
|
||||
return `
|
||||
<div class="site-message-empty">
|
||||
<span class="site-message-empty-icon" aria-hidden="true">${ICON_SEND}</span>
|
||||
<strong>${t('siteMessages.emptyAnnouncements')}</strong>
|
||||
<p>${t('siteMessages.emptyAnnouncementsHint')}</p>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
return `
|
||||
<div class="site-message-section-head">
|
||||
<span>${t('siteMessages.fixedAnnouncements')}</span>
|
||||
<small>${t('siteMessages.managedBySite')}</small>
|
||||
</div>
|
||||
<div class="site-message-announcement-list">
|
||||
${items.map((item, index) => `
|
||||
<article class="site-message-announcement level-${escapeAttr(item.level)} ${index === 0 ? 'featured' : ''}">
|
||||
<div class="site-message-announcement-main">
|
||||
<div class="site-message-announcement-kicker">
|
||||
<span>${escapeHtml(item.badge || t('siteMessages.announcementBadge'))}</span>
|
||||
<small>${renderLevelLabel(item.level)}</small>
|
||||
</div>
|
||||
<h3>${escapeHtml(item.title)}</h3>
|
||||
${item.body ? `<p>${escapeHtml(item.body)}</p>` : ''}
|
||||
</div>
|
||||
<div class="site-message-announcement-side">
|
||||
<span>${formatMessageTime(item)}</span>
|
||||
${renderMessageCta(item, true)}
|
||||
</div>
|
||||
</article>
|
||||
`).join('')}
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
function renderMessageCta(item, prominent = false) {
|
||||
if (!item.ctaText || !item.ctaUrl) return ''
|
||||
return `<a class="${prominent ? 'site-message-card-cta' : 'site-message-inline-cta'}" href="${escapeAttr(item.ctaUrl)}" target="_blank" rel="noopener">${escapeHtml(item.ctaText)}</a>`
|
||||
}
|
||||
|
||||
function bindModalEvents(overlay) {
|
||||
overlay.querySelector('.site-message-close')?.addEventListener('click', closeModal)
|
||||
overlay.querySelector('[data-site-message-today]')?.addEventListener('click', () => {
|
||||
localStorage.setItem(TODAY_CLOSE_KEY, todayKey())
|
||||
closeModal()
|
||||
})
|
||||
overlay.querySelector('[data-site-message-dismiss]')?.addEventListener('click', () => {
|
||||
dismissItems(getVisibleMessages()[_activeTab])
|
||||
closeModal()
|
||||
updateLauncherBadge()
|
||||
})
|
||||
overlay.querySelectorAll('[data-site-message-tab]').forEach((btn) => {
|
||||
btn.addEventListener('click', () => {
|
||||
_activeTab = btn.dataset.siteMessageTab || 'notifications'
|
||||
renderModal()
|
||||
})
|
||||
})
|
||||
overlay.addEventListener('click', (event) => {
|
||||
if (event.target === overlay) closeModal()
|
||||
})
|
||||
overlay.addEventListener('keydown', (event) => {
|
||||
if (event.key === 'Escape') closeModal()
|
||||
})
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
_overlay?.remove()
|
||||
_overlay = null
|
||||
}
|
||||
|
||||
function dismissItems(items) {
|
||||
for (const item of items || []) {
|
||||
const key = dismissStorageKey(item)
|
||||
if (key) localStorage.setItem(key, '1')
|
||||
}
|
||||
}
|
||||
|
||||
function shouldAutoOpen() {
|
||||
if (localStorage.getItem(TODAY_CLOSE_KEY) === todayKey()) return false
|
||||
const visible = getVisibleMessages()
|
||||
return visible.notifications.length > 0 || visible.announcements.length > 0
|
||||
}
|
||||
|
||||
function getVisibleMessages() {
|
||||
return {
|
||||
notifications: _messages.notifications.filter(item => !isDismissed(item)),
|
||||
announcements: _messages.announcements.filter(item => !isDismissed(item)),
|
||||
}
|
||||
}
|
||||
|
||||
function normalizePayload(payload = {}) {
|
||||
const notifications = []
|
||||
const announcements = []
|
||||
|
||||
const appendItem = (raw, fallbackType) => {
|
||||
if (!acceptsClientSurface(raw)) return
|
||||
const displayType = classifyDisplayType(raw, fallbackType)
|
||||
const item = normalizeItem(raw, displayType)
|
||||
if (displayType === 'notification') {
|
||||
notifications.push(item)
|
||||
} else {
|
||||
announcements.push(item)
|
||||
}
|
||||
}
|
||||
|
||||
if (Array.isArray(payload.notifications)) {
|
||||
payload.notifications.forEach(item => appendItem(item, 'notification'))
|
||||
}
|
||||
if (Array.isArray(payload.announcements)) {
|
||||
payload.announcements.forEach(item => appendItem(item, 'announcement'))
|
||||
}
|
||||
|
||||
notifications.sort(sortByTimeDesc)
|
||||
announcements.sort(sortByPriority)
|
||||
return { notifications, announcements }
|
||||
}
|
||||
|
||||
function normalizeItem(item = {}, fallbackType = 'notification') {
|
||||
const displayType = classifyDisplayType(item, fallbackType)
|
||||
const type = String(item.type || item.kind || item.category || displayType).toLowerCase()
|
||||
const level = normalizeLevel(item.level)
|
||||
return {
|
||||
id: String(item.id || item.dismissKey || `${type}-${item.title || item.updatedAt || Date.now()}`),
|
||||
type,
|
||||
displayType,
|
||||
targetSurface: String(item.targetSurface || item.surface || ''),
|
||||
level,
|
||||
title: String(item.title || t('siteMessages.defaultTitle')),
|
||||
body: String(item.body || item.content || item.summary || ''),
|
||||
badge: item.badge ? String(item.badge) : '',
|
||||
ctaText: item.ctaText ? String(item.ctaText) : '',
|
||||
ctaUrl: item.ctaUrl ? safeHref(item.ctaUrl) : '',
|
||||
dismissKey: item.dismissKey || item.id || '',
|
||||
updatedAt: item.updatedAt || item.publishedAt || item.startAt || item.createdAt || '',
|
||||
pinned: item.pinned === true || item.fixed === true || displayType === 'announcement',
|
||||
}
|
||||
}
|
||||
|
||||
function acceptsClientSurface(item = {}) {
|
||||
const surface = String(item.targetSurface || item.surface || '').trim().toLowerCase()
|
||||
return !surface || surface === 'client' || surface === 'all'
|
||||
}
|
||||
|
||||
function classifyDisplayType(item = {}, fallbackType = 'notification') {
|
||||
const raw = String(item.displayType || item.display_type || item.type || item.kind || item.category || fallbackType)
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
if (['notification', 'notice', 'message', 'feed', 'timeline'].includes(raw)) return 'notification'
|
||||
return 'announcement'
|
||||
}
|
||||
|
||||
function normalizeLevel(level) {
|
||||
const next = String(level || 'info').toLowerCase()
|
||||
return ['critical', 'warning', 'success', 'info'].includes(next) ? next : 'info'
|
||||
}
|
||||
|
||||
function sortByTimeDesc(a, b) {
|
||||
return toTime(b.updatedAt) - toTime(a.updatedAt)
|
||||
}
|
||||
|
||||
function sortByPriority(a, b) {
|
||||
const weight = { critical: 3, warning: 2, success: 1, info: 0 }
|
||||
return (weight[b.level] || 0) - (weight[a.level] || 0) || sortByTimeDesc(a, b)
|
||||
}
|
||||
|
||||
function formatMessageTime(item) {
|
||||
const time = toTime(item.updatedAt)
|
||||
if (!time) return t('siteMessages.timeUnknown')
|
||||
const date = new Date(time)
|
||||
const now = Date.now()
|
||||
const diffDays = Math.floor((now - time) / 86400000)
|
||||
const absolute = `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`
|
||||
if (diffDays <= 0) return `${t('siteMessages.today')} ${absolute}`
|
||||
if (diffDays < 30) return `${diffDays}${t('siteMessages.daysAgo')} ${absolute}`
|
||||
const months = Math.max(1, Math.floor(diffDays / 30))
|
||||
return `${months}${t('siteMessages.monthsAgo')} ${absolute}`
|
||||
}
|
||||
|
||||
function formatSummary(visible) {
|
||||
return t('siteMessages.summary', {
|
||||
notifications: visible.notifications.length,
|
||||
announcements: visible.announcements.length,
|
||||
})
|
||||
}
|
||||
|
||||
function renderLevelLabel(level) {
|
||||
const labels = {
|
||||
critical: t('siteMessages.levelCritical'),
|
||||
warning: t('siteMessages.levelWarning'),
|
||||
success: t('siteMessages.levelSuccess'),
|
||||
info: t('siteMessages.levelInfo'),
|
||||
}
|
||||
return labels[level] || labels.info
|
||||
}
|
||||
|
||||
function dismissStorageKey(item) {
|
||||
const key = item?.dismissKey || item?.id
|
||||
return key ? `${DISMISS_PREFIX}${key}` : ''
|
||||
}
|
||||
|
||||
function isDismissed(item) {
|
||||
const key = dismissStorageKey(item)
|
||||
return !!key && localStorage.getItem(key) === '1'
|
||||
}
|
||||
|
||||
function todayKey() {
|
||||
return new Date().toISOString().slice(0, 10)
|
||||
}
|
||||
|
||||
function toTime(value) {
|
||||
if (!value) return 0
|
||||
const time = new Date(value).getTime()
|
||||
return Number.isFinite(time) ? time : 0
|
||||
}
|
||||
|
||||
function pad(value) {
|
||||
return String(value).padStart(2, '0')
|
||||
}
|
||||
|
||||
function safeHref(raw) {
|
||||
try {
|
||||
const url = new URL(String(raw || '').trim())
|
||||
if (url.protocol === 'https:' || url.protocol === 'http:' || url.protocol === 'mailto:') {
|
||||
return url.toString()
|
||||
}
|
||||
} catch {}
|
||||
return ''
|
||||
}
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value || '')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
}
|
||||
|
||||
function escapeAttr(value) {
|
||||
return escapeHtml(value).replace(/'/g, ''')
|
||||
}
|
||||
@@ -1215,19 +1215,19 @@ body[data-active-engine="hermes"] .sidebar-footer {
|
||||
border-top: 1px solid rgba(28, 25, 23, 0.06);
|
||||
padding: 8px 10px;
|
||||
}
|
||||
body[data-active-engine="hermes"] #btn-theme-toggle,
|
||||
body[data-active-engine="hermes"] .lang-trigger {
|
||||
min-height: 44px;
|
||||
padding: 8px 14px;
|
||||
border-radius: 6px;
|
||||
body[data-active-engine="hermes"] .sidebar-tools > .sidebar-tool-btn,
|
||||
body[data-active-engine="hermes"] .sidebar-tools > .lang-switcher > .sidebar-tool-btn {
|
||||
min-height: 32px;
|
||||
padding: 0;
|
||||
border-radius: 9px;
|
||||
color: #78716C;
|
||||
font-size: 12.5px;
|
||||
transition: background 180ms, color 180ms;
|
||||
}
|
||||
body[data-active-engine="hermes"] #btn-theme-toggle:hover,
|
||||
body[data-active-engine="hermes"] .lang-trigger:hover {
|
||||
body[data-active-engine="hermes"] .sidebar-tools > .sidebar-tool-btn:hover,
|
||||
body[data-active-engine="hermes"] .sidebar-tools > .lang-switcher > .sidebar-tool-btn:hover {
|
||||
background: rgba(28, 25, 23, 0.04);
|
||||
color: #1C1917;
|
||||
color: #CA8A04;
|
||||
}
|
||||
body[data-active-engine="hermes"] .lang-dropdown {
|
||||
background: #FFFFFF;
|
||||
@@ -1345,14 +1345,14 @@ body[data-active-engine="hermes"][data-theme="dark"] {
|
||||
[data-theme="dark"] body[data-active-engine="hermes"] .sidebar-footer {
|
||||
border-top-color: rgba(250, 250, 249, 0.06);
|
||||
}
|
||||
[data-theme="dark"] body[data-active-engine="hermes"] #btn-theme-toggle,
|
||||
[data-theme="dark"] body[data-active-engine="hermes"] .lang-trigger {
|
||||
[data-theme="dark"] body[data-active-engine="hermes"] .sidebar-tools > .sidebar-tool-btn,
|
||||
[data-theme="dark"] body[data-active-engine="hermes"] .sidebar-tools > .lang-switcher > .sidebar-tool-btn {
|
||||
color: #A8A29E;
|
||||
}
|
||||
[data-theme="dark"] body[data-active-engine="hermes"] #btn-theme-toggle:hover,
|
||||
[data-theme="dark"] body[data-active-engine="hermes"] .lang-trigger:hover {
|
||||
[data-theme="dark"] body[data-active-engine="hermes"] .sidebar-tools > .sidebar-tool-btn:hover,
|
||||
[data-theme="dark"] body[data-active-engine="hermes"] .sidebar-tools > .lang-switcher > .sidebar-tool-btn:hover {
|
||||
background: rgba(250, 250, 249, 0.05);
|
||||
color: #FAFAF9;
|
||||
color: #EAB308;
|
||||
}
|
||||
[data-theme="dark"] body[data-active-engine="hermes"] .lang-dropdown {
|
||||
background: #1C1917;
|
||||
|
||||
@@ -1068,18 +1068,19 @@ body[data-active-engine="xintian"] .sidebar-footer {
|
||||
border-top: 1px solid rgba(27, 20, 16, 0.06);
|
||||
padding: 8px 10px;
|
||||
}
|
||||
body[data-active-engine="xintian"] #btn-theme-toggle,
|
||||
body[data-active-engine="xintian"] .lang-trigger {
|
||||
padding: 8px 14px;
|
||||
border-radius: 6px;
|
||||
body[data-active-engine="xintian"] .sidebar-tools > .sidebar-tool-btn,
|
||||
body[data-active-engine="xintian"] .sidebar-tools > .lang-switcher > .sidebar-tool-btn {
|
||||
min-height: 32px;
|
||||
padding: 0;
|
||||
border-radius: 9px;
|
||||
color: #7A6A5B;
|
||||
font-size: 12.5px;
|
||||
transition: background 180ms, color 180ms;
|
||||
}
|
||||
body[data-active-engine="xintian"] #btn-theme-toggle:hover,
|
||||
body[data-active-engine="xintian"] .lang-trigger:hover {
|
||||
body[data-active-engine="xintian"] .sidebar-tools > .sidebar-tool-btn:hover,
|
||||
body[data-active-engine="xintian"] .sidebar-tools > .lang-switcher > .sidebar-tool-btn:hover {
|
||||
background: rgba(27, 20, 16, 0.04);
|
||||
color: #1B1410;
|
||||
color: #EA580C;
|
||||
}
|
||||
body[data-active-engine="xintian"] .lang-dropdown {
|
||||
background: #FFFFFF;
|
||||
@@ -1181,14 +1182,14 @@ body[data-active-engine="xintian"] .sidebar-version { color: #A99A8B; }
|
||||
[data-theme="dark"] body[data-active-engine="xintian"] .sidebar-footer {
|
||||
border-top-color: rgba(255, 244, 230, 0.06);
|
||||
}
|
||||
[data-theme="dark"] body[data-active-engine="xintian"] #btn-theme-toggle,
|
||||
[data-theme="dark"] body[data-active-engine="xintian"] .lang-trigger {
|
||||
[data-theme="dark"] body[data-active-engine="xintian"] .sidebar-tools > .sidebar-tool-btn,
|
||||
[data-theme="dark"] body[data-active-engine="xintian"] .sidebar-tools > .lang-switcher > .sidebar-tool-btn {
|
||||
color: #B39C83;
|
||||
}
|
||||
[data-theme="dark"] body[data-active-engine="xintian"] #btn-theme-toggle:hover,
|
||||
[data-theme="dark"] body[data-active-engine="xintian"] .lang-trigger:hover {
|
||||
[data-theme="dark"] body[data-active-engine="xintian"] .sidebar-tools > .sidebar-tool-btn:hover,
|
||||
[data-theme="dark"] body[data-active-engine="xintian"] .sidebar-tools > .lang-switcher > .sidebar-tool-btn:hover {
|
||||
background: rgba(255, 244, 230, 0.05);
|
||||
color: #FFF4E6;
|
||||
color: #FB923C;
|
||||
}
|
||||
[data-theme="dark"] body[data-active-engine="xintian"] .lang-dropdown {
|
||||
background: #2E1F16;
|
||||
|
||||
@@ -85,6 +85,11 @@ export function clearRequestLogs() {
|
||||
_requestLogs.length = 0
|
||||
}
|
||||
|
||||
function normalizeSiteLocale(locale) {
|
||||
const value = String(locale || '').trim().toLowerCase()
|
||||
return value.startsWith('zh') ? 'zh-CN' : 'en'
|
||||
}
|
||||
|
||||
function cachedInvoke(cmd, args = {}, ttl = CACHE_TTL) {
|
||||
const key = cmd + JSON.stringify(args)
|
||||
const cached = _cache.get(key)
|
||||
@@ -395,6 +400,7 @@ export const api = {
|
||||
getDeployConfig: () => cachedInvoke('get_deploy_config'),
|
||||
patchModelVision: () => invoke('patch_model_vision'),
|
||||
checkPanelUpdate: () => invoke('check_panel_update'),
|
||||
checkSiteAnnouncements: (locale) => invoke('check_site_announcements', { locale: normalizeSiteLocale(locale) }),
|
||||
writeEnvFile: (path, config) => invoke('write_env_file', { path, config }),
|
||||
|
||||
// 备份管理
|
||||
|
||||
@@ -41,13 +41,14 @@ import glossary from './modules/glossary.js'
|
||||
import hermesLazyDeps from './modules/hermesLazyDeps.js'
|
||||
import notifications from './modules/notifications.js'
|
||||
import kernel from './modules/kernel.js'
|
||||
import siteMessages from './modules/siteMessages.js'
|
||||
|
||||
const MODULES = {
|
||||
common, sidebar, instance, dashboard, services, settings,
|
||||
models, agents, agentDetail, gateway, security, communication, channels,
|
||||
memory, dreaming, cron, usage, skills, chat, chatDebug, setup, about,
|
||||
ext, logs, assistant, toast, modal, engagement, diagnose, routeMap, extensions,
|
||||
engine, ciaoBug, cliConflict, glossary, hermesLazyDeps, notifications, kernel,
|
||||
engine, ciaoBug, cliConflict, glossary, hermesLazyDeps, notifications, kernel, siteMessages,
|
||||
}
|
||||
|
||||
/** 判断是否是 _() 调用产生的翻译对象(有 'zh-CN' 字符串字段) */
|
||||
|
||||
@@ -82,6 +82,7 @@ export default {
|
||||
versionAvailable: _('ClawPanel v{version} 可用', 'ClawPanel v{version} available', 'ClawPanel v{version} 可用', 'ClawPanel v{version} が利用可能です', 'ClawPanel v{version} 사용 가능', 'ClawPanel v{version} đã có sẵn'),
|
||||
updateMethod: _('更新方法', 'Update Method', '更新方法', '更新方法', '업데이트 방법', 'Cách cập nhật'),
|
||||
releaseNotes: _('更新日志', 'Release Notes', '更新日誌', 'リリースノート', '릴리스 노트', 'Ghi chú phát hành'),
|
||||
viewUpdateDetails: _('查看更新详情', 'View update details', '查看更新詳情', '更新の詳細を見る', '업데이트 상세 보기', 'Xem chi tiết cập nhật'),
|
||||
dismissVersion: _('忽略此版本', 'Ignore this version', '忽略此版本', 'このバージョンを無視', '이 버전 무시', 'Bỏ qua phiên bản này'),
|
||||
updateToVersion: _('更新到 v{version}', 'Update to v{version}', '更新到 v{version}', 'v{version} に更新', 'v{version}(으)로 업데이트', 'Cập nhật lên v{version}'),
|
||||
runOnServer: _('在服务器上执行以下命令:', 'Run the following commands on the server:', '在伺服器上執行以下命令:', 'サーバーで次のコマンドを実行してください:', '서버에서 다음 명령을 실행하세요:', 'Hãy chạy các lệnh sau trên máy chủ:'),
|
||||
@@ -91,14 +92,25 @@ export default {
|
||||
downloadFromGitHub: _('GitHub 下载', 'GitHub Download', 'GitHub 下載'),
|
||||
newVersionAvailable: _('发现新版本 v{version},请前往下载更新', 'New version v{version} available, please download to update', '發現新版本 v{version},請前往下載更新'),
|
||||
versionMismatch: _('前端版本 v{frontend} 与应用版本 v{binary} 不一致', 'Frontend v{frontend} does not match app v{binary}', '前端版本 v{frontend} 與應用版本 v{binary} 不一致', 'フロントエンド v{frontend} とアプリ v{binary} が一致しません', '프런트엔드 v{frontend}과 앱 v{binary}이 일치하지 않습니다'),
|
||||
hotUpdateDeprecated: _('热更新已弃用,请下载完整安装包以获得最佳体验', 'Hot update is deprecated, please download the full installer for the best experience', '熱更新已棄用,請下載完整安裝包以獲得最佳體驗', 'ホットアップデートは非推奨です。最高の体験のためにフルインストーラーをダウンロードしてください', '핫 업데이트는 더 이상 사용되지 않습니다. 최상의 경험을 위해 전체 설치 프로그램을 다운로드하세요'),
|
||||
hotUpdateNow: _('立即热更新', 'Hot Update Now', '立即熱更新', '今すぐホットアップデート', '지금 핫 업데이트'),
|
||||
hotUpdateDownloading: _('正在下载更新...', 'Downloading update...', '正在下載更新...', '更新をダウンロード中...', '업데이트 다운로드 중...'),
|
||||
hotUpdateDone: _('更新已下载,重启后生效', 'Update downloaded, restart to apply', '更新已下載,重啟後生效', '更新をダウンロードしました。再起動して適用してください', '업데이트가 다운로드되었습니다. 적용하려면 다시 시작하세요.'),
|
||||
hotUpdateFailed: _('更新下载失败', 'Update download failed', '更新下載失敗', '更新のダウンロードに失敗しました', '업데이트 다운로드 실패'),
|
||||
restartApp: _('重启应用', 'Restart App', '重啟應用', 'アプリを再起動', '앱 다시 시작'),
|
||||
restartLater: _('稍后重启', 'Restart Later', '稍後重啟', '後で再起動', '나중에 다시 시작'),
|
||||
downloadFullInstaller: _('下载完整安装包', 'Download Full Installer', '下載完整安裝包', 'フルインストーラーをダウンロード', '전체 설치 프로그램 다운로드'),
|
||||
downloadRecommendedInstaller: _('下载推荐安装包', 'Download recommended installer', '下載推薦安裝包'),
|
||||
installerUpdateKicker: _('完整安装包更新', 'Full installer update', '完整安裝包更新'),
|
||||
installerUpdateSubtitle: _('请下载官网推荐安装包,关闭当前应用后按系统提示安装。', 'Download the recommended installer, close the app, then install it using your system installer.', '請下載官網推薦安裝包,關閉目前應用後按系統提示安裝。'),
|
||||
currentAppVersion: _('当前应用版本', 'Current app version', '目前應用版本'),
|
||||
latestAppVersion: _('最新稳定版本', 'Latest stable version', '最新穩定版本'),
|
||||
recommendedInstaller: _('推荐安装包', 'Recommended installer', '推薦安裝包'),
|
||||
installerStepsTitle: _('安装引导', 'Install guide', '安裝引導'),
|
||||
installerStepDownload: _('点击“下载推荐安装包”,浏览器会从官网镜像获取适合当前系统的文件。', 'Click “Download recommended installer”; your browser will fetch the right file from the official mirror.', '點擊「下載推薦安裝包」,瀏覽器會從官網鏡像取得適合目前系統的檔案。'),
|
||||
installerStepWindows: _('Windows:退出 ClawPanel 后双击 .exe / .msi 安装,按安装向导覆盖更新即可。', 'Windows: quit ClawPanel, run the .exe / .msi installer, and follow the wizard to update.', 'Windows:退出 ClawPanel 後雙擊 .exe / .msi 安裝,按安裝精靈覆蓋更新即可。'),
|
||||
installerStepMacos: _('macOS:打开 .dmg,把 ClawPanel 拖入 Applications;如系统拦截,可右键打开或在隐私与安全性中允许。', 'macOS: open the .dmg and drag ClawPanel into Applications. If blocked, right-click Open or allow it in Privacy & Security.', 'macOS:打開 .dmg,把 ClawPanel 拖入 Applications;如系統攔截,可右鍵打開或在隱私與安全性中允許。'),
|
||||
installerStepLinuxAppImage: _('Linux AppImage:下载后添加可执行权限,然后运行新文件;也可以替换原来的 AppImage。', 'Linux AppImage: make the file executable, then run it; you can replace the old AppImage if needed.', 'Linux AppImage:下載後加入可執行權限,然後執行新檔案;也可以替換原來的 AppImage。'),
|
||||
installerStepLinuxDeb: _('Linux deb:下载后用系统软件安装器打开,或用 dpkg / apt 覆盖安装。', 'Linux deb: open it with your software installer, or install it with dpkg / apt.', 'Linux deb:下載後用系統軟體安裝器開啟,或用 dpkg / apt 覆蓋安裝。'),
|
||||
installerStepLinuxRpm: _('Linux rpm:下载后用系统软件安装器打开,或用 rpm / dnf 覆盖安装。', 'Linux rpm: open it with your software installer, or install it with rpm / dnf.', 'Linux rpm:下載後用系統軟體安裝器開啟,或用 rpm / dnf 覆蓋安裝。'),
|
||||
installerStepLinux: _('Linux:下载对应发行版安装包后,用系统软件安装器或包管理器覆盖安装。', 'Linux: download the right package and update it with your software installer or package manager.', 'Linux:下載對應發行版安裝包後,用系統軟體安裝器或包管理器覆蓋安裝。'),
|
||||
installerStepGeneric: _('下载完成后关闭 ClawPanel,使用系统默认安装器打开文件并完成覆盖安装。', 'After downloading, quit ClawPanel and open the file with the system installer to update.', '下載完成後關閉 ClawPanel,使用系統預設安裝器打開檔案並完成覆蓋安裝。'),
|
||||
installerStepRestart: _('安装完成后重新打开 ClawPanel,关于页显示新版本即更新完成。', 'Reopen ClawPanel after installation; the About page should show the new version.', '安裝完成後重新打開 ClawPanel,關於頁顯示新版本即更新完成。'),
|
||||
installerOnlyUpdateHint: _('当前版本请使用完整安装包更新。', 'Use the full installer for this version.', '目前版本請使用完整安裝包更新。'),
|
||||
remindLater: _('稍后提醒', 'Remind me later', '稍後提醒'),
|
||||
upToDate: _('已是最新', 'Up to date', '', '最新です', '최신 상태', 'Đã cập nhật', 'Actualizado', 'Atualizado', 'Актуально', 'À jour', 'Aktuell'),
|
||||
checkUpdateFailed: _('暂无法检查更新', 'Unable to check for updates', '暫無法檢查更新', '更新を確認できません', '업데이트 확인 실패', 'Kiểm tra cập nhật thất bại', 'Error al verificar actualizaciones', 'Falha ao verificar atualizações', 'Ошибка проверки обновлений', 'Échec de la vérification des mises à jour', 'Update-Prüfung fehlgeschlagen'),
|
||||
qqGroup: _('QQ 交流群', 'QQ Group'),
|
||||
|
||||
28
src/locales/modules/siteMessages.js
Normal file
28
src/locales/modules/siteMessages.js
Normal file
@@ -0,0 +1,28 @@
|
||||
import { _ } from '../helper.js'
|
||||
|
||||
export default {
|
||||
title: _('系统公告', 'System notices'),
|
||||
summary: _('通知 {notifications} 条 · 公告 {announcements} 条', '{notifications} notifications · {announcements} announcements'),
|
||||
notifications: _('通知', 'Notifications'),
|
||||
announcements: _('系统公告', 'Announcements'),
|
||||
announcementBadge: _('公告', 'Announcement'),
|
||||
notificationFeed: _('通知动态', 'Notification feed'),
|
||||
fixedAnnouncements: _('固定公告', 'Pinned announcements'),
|
||||
sortedByTime: _('按时间倒序', 'Newest first'),
|
||||
managedBySite: _('由官网统一发布', 'Published by the official site'),
|
||||
levelCritical: _('紧急', 'Critical'),
|
||||
levelWarning: _('重要', 'Important'),
|
||||
levelSuccess: _('正常', 'Normal'),
|
||||
levelInfo: _('提示', 'Info'),
|
||||
closeToday: _('今日关闭', 'Close today'),
|
||||
closeCurrent: _('关闭公告', 'Close notices'),
|
||||
emptyNotifications: _('暂无通知', 'No notifications'),
|
||||
emptyNotificationsHint: _('重要通知会按时间显示在这里。', 'Important notices will appear here in chronological order.'),
|
||||
emptyAnnouncements: _('暂无系统公告', 'No announcements'),
|
||||
emptyAnnouncementsHint: _('官网固定公告会显示在这里。', 'Pinned official announcements will appear here.'),
|
||||
defaultTitle: _('ClawPanel 通知', 'ClawPanel notice'),
|
||||
timeUnknown: _('时间未知', 'Unknown time'),
|
||||
today: _('今天', 'Today'),
|
||||
daysAgo: _(' 天前', ' days ago'),
|
||||
monthsAgo: _(' 个月前', ' months ago'),
|
||||
}
|
||||
412
src/main.js
412
src/main.js
@@ -16,11 +16,13 @@ import { statusIcon } from './lib/icons.js'
|
||||
import { isForeignGatewayError, showGatewayConflictGuidance } from './lib/gateway-ownership.js'
|
||||
import { tryShowEngagement } from './components/engagement.js'
|
||||
import { toast } from './components/toast.js'
|
||||
import { initI18n, t } from './lib/i18n.js'
|
||||
import { initI18n, t, getLang, setLang, getAvailableLangs } from './lib/i18n.js'
|
||||
import { renderMarkdown } from './lib/markdown.js'
|
||||
import { initFeatureGates } from './lib/feature-gates.js'
|
||||
import { onKernelChange } from './lib/kernel.js'
|
||||
import { showFloorBlocker, hideFloorBlocker } from './components/floor-blocker.js'
|
||||
import { registerEngine, initEngineManager, getActiveEngine, getActiveEngineId, needsInitialEngineChoice, isEngineSetupDeferred, adoptActiveEngineSelection, onEngineChange } from './lib/engine-manager.js'
|
||||
import { initSiteMessageCenter, refreshSiteMessageCenter } from './components/site-message-center.js'
|
||||
import openclawEngine from './engines/openclaw/index.js'
|
||||
import hermesEngine from './engines/hermes/index.js'
|
||||
import xintianEngine from './engines/xintian/index.js'
|
||||
@@ -36,6 +38,7 @@ import './style/agents.css'
|
||||
import './style/debug.css'
|
||||
import './style/assistant.css'
|
||||
import './style/ai-drawer.css'
|
||||
import './style/site-message-center.css'
|
||||
// 引擎专属样式(scope 到 [data-engine="<id>"] 子树,不影响其他引擎)
|
||||
import './engines/hermes/style/hermes.css'
|
||||
import './engines/xintian/style/xintian.css'
|
||||
@@ -49,6 +52,268 @@ function escapeHtml(str) {
|
||||
return (str || '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"')
|
||||
}
|
||||
|
||||
function sanitizeReleaseNotesHtml(html) {
|
||||
if (!html || typeof DOMParser === 'undefined') return ''
|
||||
const doc = new DOMParser().parseFromString(`<div>${html}</div>`, 'text/html')
|
||||
const root = doc.body.firstElementChild
|
||||
const allowedTags = new Set([
|
||||
'A', 'BLOCKQUOTE', 'BR', 'CODE', 'DIV', 'EM', 'H1', 'H2', 'H3', 'H4',
|
||||
'LI', 'OL', 'P', 'PRE', 'SPAN', 'STRONG', 'TABLE', 'TBODY', 'TD',
|
||||
'TH', 'THEAD', 'TR', 'UL',
|
||||
])
|
||||
const blockedTags = new Set(['EMBED', 'IFRAME', 'MATH', 'OBJECT', 'SCRIPT', 'STYLE', 'SVG'])
|
||||
|
||||
const walk = (node) => {
|
||||
for (const child of Array.from(node.children)) {
|
||||
if (blockedTags.has(child.tagName)) {
|
||||
child.remove()
|
||||
continue
|
||||
}
|
||||
if (!allowedTags.has(child.tagName)) {
|
||||
walk(child)
|
||||
child.replaceWith(...Array.from(child.childNodes))
|
||||
continue
|
||||
}
|
||||
|
||||
for (const attr of Array.from(child.attributes)) {
|
||||
const name = attr.name.toLowerCase()
|
||||
if (name.startsWith('on') || name === 'style') {
|
||||
child.removeAttribute(attr.name)
|
||||
}
|
||||
}
|
||||
|
||||
if (child.tagName === 'A') {
|
||||
const href = child.getAttribute('href') || ''
|
||||
const safe = safeExternalHref(href, '')
|
||||
if (safe) {
|
||||
child.setAttribute('href', safe)
|
||||
child.setAttribute('target', '_blank')
|
||||
child.setAttribute('rel', 'noopener')
|
||||
} else {
|
||||
child.removeAttribute('href')
|
||||
}
|
||||
} else {
|
||||
child.removeAttribute('href')
|
||||
child.removeAttribute('src')
|
||||
child.removeAttribute('target')
|
||||
child.removeAttribute('rel')
|
||||
}
|
||||
|
||||
walk(child)
|
||||
}
|
||||
}
|
||||
|
||||
walk(root)
|
||||
return root.innerHTML
|
||||
}
|
||||
|
||||
function renderReleaseNotesMarkdown(markdown) {
|
||||
const text = String(markdown || '').trim()
|
||||
if (!text) return ''
|
||||
return sanitizeReleaseNotesHtml(renderMarkdown(text))
|
||||
}
|
||||
|
||||
function safeExternalHref(raw, fallback = 'https://claw.qt.cool') {
|
||||
try {
|
||||
const url = new URL(String(raw || '').trim() || fallback, 'https://claw.qt.cool')
|
||||
const host = url.hostname.toLowerCase()
|
||||
if (host === 'claw.qt.cool') {
|
||||
url.protocol = 'https:'
|
||||
return url.toString()
|
||||
}
|
||||
if ((host === 'github.com' || host === 'api.github.com') && url.protocol === 'https:') {
|
||||
return url.toString()
|
||||
}
|
||||
} catch {}
|
||||
return fallback
|
||||
}
|
||||
|
||||
function compareVersions(a, b) {
|
||||
const parse = (value) => String(value || '').replace(/^v/i, '').split(/[^0-9]/).filter(Boolean).map(Number)
|
||||
const pa = parse(a)
|
||||
const pb = parse(b)
|
||||
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
||||
const av = pa[i] || 0
|
||||
const bv = pb[i] || 0
|
||||
if (av > bv) return 1
|
||||
if (av < bv) return -1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
async function getInstalledAppVersion() {
|
||||
if (!isTauriRuntime()) return APP_VERSION
|
||||
try {
|
||||
const { getVersion } = await import('@tauri-apps/api/app')
|
||||
return await getVersion()
|
||||
} catch {
|
||||
return APP_VERSION
|
||||
}
|
||||
}
|
||||
|
||||
function formatBytes(value, fallback = '') {
|
||||
const size = Number(value)
|
||||
if (!Number.isFinite(size) || size <= 0) return fallback
|
||||
const units = ['B', 'KB', 'MB', 'GB']
|
||||
let next = size
|
||||
let unit = 0
|
||||
while (next >= 1024 && unit < units.length - 1) {
|
||||
next /= 1024
|
||||
unit += 1
|
||||
}
|
||||
return `${next >= 10 || unit === 0 ? next.toFixed(0) : next.toFixed(1)} ${units[unit]}`
|
||||
}
|
||||
|
||||
function getInstallerAsset(panelInfo) {
|
||||
const asset = panelInfo?.recommendedAsset
|
||||
return asset && typeof asset === 'object' ? asset : null
|
||||
}
|
||||
|
||||
function getInstallerUrl(panelInfo) {
|
||||
return safeExternalHref(
|
||||
panelInfo?.recommendedAsset?.downloadUrl || panelInfo?.downloadUrl || 'https://claw.qt.cool'
|
||||
)
|
||||
}
|
||||
|
||||
function getGitHubReleaseUrl(panelInfo) {
|
||||
if (panelInfo?.source === 'site') return 'https://github.com/qingchencloud/clawpanel/releases'
|
||||
return safeExternalHref(panelInfo?.url, 'https://github.com/qingchencloud/clawpanel/releases')
|
||||
}
|
||||
|
||||
function getInstallerStepKey(asset) {
|
||||
const platform = String(asset?.platform || '').toLowerCase()
|
||||
const fileType = String(asset?.fileType || '').toLowerCase()
|
||||
const name = String(asset?.name || '').toLowerCase()
|
||||
if (platform === 'windows' || fileType === 'exe' || fileType === 'msi') return 'about.installerStepWindows'
|
||||
if (platform === 'macos' || fileType === 'dmg') return 'about.installerStepMacos'
|
||||
if (platform === 'linux') {
|
||||
if (fileType === 'deb') return 'about.installerStepLinuxDeb'
|
||||
if (fileType === 'rpm') return 'about.installerStepLinuxRpm'
|
||||
if (fileType === 'appimage' || name.endsWith('.appimage')) return 'about.installerStepLinuxAppImage'
|
||||
return 'about.installerStepLinux'
|
||||
}
|
||||
return 'about.installerStepGeneric'
|
||||
}
|
||||
|
||||
function formatInstallerMeta(asset) {
|
||||
if (!asset) return ''
|
||||
const parts = []
|
||||
const platform = [asset.platform, asset.arch].filter(Boolean).join(' / ')
|
||||
const size = asset.sizeText || formatBytes(asset.size)
|
||||
if (platform) parts.push(platform)
|
||||
if (asset.fileType) parts.push(String(asset.fileType).toUpperCase())
|
||||
if (size) parts.push(size)
|
||||
if (asset.sourceText) parts.push(asset.sourceText)
|
||||
return parts.join(' · ')
|
||||
}
|
||||
|
||||
function showInstallerUpdateModal({ panelInfo, latest, installedVersion }) {
|
||||
const old = document.getElementById('installer-update-overlay')
|
||||
if (old) old.remove()
|
||||
|
||||
const asset = getInstallerAsset(panelInfo)
|
||||
const downloadUrl = getInstallerUrl(panelInfo)
|
||||
const githubUrl = getGitHubReleaseUrl(panelInfo)
|
||||
const assetName = asset?.name || t('about.downloadFullInstaller')
|
||||
const assetMeta = formatInstallerMeta(asset)
|
||||
const changelog = String(panelInfo?.releaseNotes || '').trim()
|
||||
const versionKey = String(latest || '').trim()
|
||||
const sessionKey = versionKey ? `clawpanel_update_seen_session_${versionKey}` : ''
|
||||
|
||||
const overlay = document.createElement('div')
|
||||
overlay.id = 'installer-update-overlay'
|
||||
overlay.className = 'modal-overlay installer-update-overlay'
|
||||
overlay.setAttribute('role', 'dialog')
|
||||
overlay.setAttribute('aria-modal', 'true')
|
||||
overlay.innerHTML = `
|
||||
<div class="modal installer-update-modal" tabindex="-1">
|
||||
<button class="installer-update-close" id="btn-installer-update-close" title="${t('common.close')}">×</button>
|
||||
<div class="installer-update-head">
|
||||
<div class="installer-update-icon" aria-hidden="true">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
|
||||
<path d="M7 10l5 5 5-5"/>
|
||||
<path d="M12 15V3"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="installer-update-title-wrap">
|
||||
<div class="installer-update-kicker">${t('about.installerUpdateKicker')}</div>
|
||||
<div class="installer-update-title">${t('about.versionAvailable', { version: latest })}</div>
|
||||
<div class="installer-update-subtitle">${t('about.installerUpdateSubtitle')}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="installer-update-grid">
|
||||
<div class="installer-update-version-card">
|
||||
<span>${t('about.currentAppVersion')}</span>
|
||||
<strong>v${escapeHtml(installedVersion || APP_VERSION)}</strong>
|
||||
</div>
|
||||
<div class="installer-update-version-card installer-update-version-card-new">
|
||||
<span>${t('about.latestAppVersion')}</span>
|
||||
<strong>v${escapeHtml(latest)}</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="installer-update-asset">
|
||||
<div class="installer-update-asset-label">${t('about.recommendedInstaller')}</div>
|
||||
<div class="installer-update-asset-name">${escapeHtml(assetName)}</div>
|
||||
${assetMeta ? `<div class="installer-update-asset-meta">${escapeHtml(assetMeta)}</div>` : ''}
|
||||
</div>
|
||||
|
||||
<div class="installer-update-steps">
|
||||
<div class="installer-update-section-title">${t('about.installerStepsTitle')}</div>
|
||||
<ol>
|
||||
<li>${t('about.installerStepDownload')}</li>
|
||||
<li>${t(getInstallerStepKey(asset))}</li>
|
||||
<li>${t('about.installerStepRestart')}</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
${changelog ? `
|
||||
<div class="installer-update-notes">
|
||||
<div class="installer-update-section-title">${t('about.releaseNotes')}</div>
|
||||
<div class="installer-update-release-text installer-update-release-markdown">${renderReleaseNotesMarkdown(changelog)}</div>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<div class="installer-update-actions">
|
||||
<button class="btn btn-secondary btn-sm" id="btn-installer-update-later">${t('about.remindLater')}</button>
|
||||
<button class="btn btn-secondary btn-sm" id="btn-installer-update-ignore">${t('about.dismissVersion')}</button>
|
||||
<a class="btn btn-secondary btn-sm" id="btn-installer-update-github" href="${escapeHtml(githubUrl)}" target="_blank" rel="noopener">${t('about.downloadFromGitHub')}</a>
|
||||
<a class="btn btn-primary btn-sm" id="btn-installer-update-download" href="${escapeHtml(downloadUrl)}" target="_blank" rel="noopener">${t('about.downloadRecommendedInstaller')}</a>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
|
||||
const closeForSession = () => {
|
||||
if (sessionKey) sessionStorage.setItem(sessionKey, '1')
|
||||
overlay.remove()
|
||||
}
|
||||
const ignoreVersion = () => {
|
||||
if (versionKey) localStorage.setItem('clawpanel_update_dismissed', versionKey)
|
||||
overlay.remove()
|
||||
}
|
||||
|
||||
document.body.appendChild(overlay)
|
||||
overlay.querySelector('#btn-installer-update-close')?.addEventListener('click', closeForSession)
|
||||
overlay.querySelector('#btn-installer-update-later')?.addEventListener('click', closeForSession)
|
||||
overlay.querySelector('#btn-installer-update-ignore')?.addEventListener('click', ignoreVersion)
|
||||
overlay.querySelector('#btn-installer-update-download')?.addEventListener('click', closeForSession)
|
||||
overlay.querySelector('#btn-installer-update-github')?.addEventListener('click', closeForSession)
|
||||
overlay.addEventListener('click', (event) => {
|
||||
if (event.target === overlay) closeForSession()
|
||||
})
|
||||
overlay.addEventListener('keydown', (event) => {
|
||||
if (event.key === 'Escape') closeForSession()
|
||||
})
|
||||
const modal = overlay.querySelector('.installer-update-modal')
|
||||
if (modal) {
|
||||
modal.scrollTop = 0
|
||||
modal.focus({ preventScroll: true })
|
||||
requestAnimationFrame(() => { modal.scrollTop = 0 })
|
||||
}
|
||||
}
|
||||
|
||||
async function openGatewayConflict(error = null) {
|
||||
const services = await api.getServicesStatus().catch(() => [])
|
||||
const gw = services?.find?.(s => s.label === 'ai.openclaw.gateway') || services?.[0] || null
|
||||
@@ -175,6 +440,24 @@ function _genCaptcha() {
|
||||
return { q: `${a} + ${b} = ?`, a: a + b }
|
||||
}
|
||||
|
||||
function renderLoginLanguageSwitch() {
|
||||
const langs = getAvailableLangs()
|
||||
const current = getLang()
|
||||
const options = langs.map(lang => `
|
||||
<option value="${escapeHtml(lang.code)}" ${lang.code === current ? 'selected' : ''}>
|
||||
${escapeHtml(lang.label)} · ${escapeHtml(lang.code)}
|
||||
</option>
|
||||
`).join('')
|
||||
return `
|
||||
<label class="login-lang-switch">
|
||||
<span>Language</span>
|
||||
<select id="login-lang-select" aria-label="Language">
|
||||
${options}
|
||||
</select>
|
||||
</label>
|
||||
`
|
||||
}
|
||||
|
||||
function showLoginOverlay(defaultPw) {
|
||||
const hasDefault = !!defaultPw
|
||||
const overlay = document.createElement('div')
|
||||
@@ -185,6 +468,7 @@ function showLoginOverlay(defaultPw) {
|
||||
const resetPath = '<code style="background:rgba(99,102,241,.1);padding:2px 6px;border-radius:3px;font-size:10px;word-break:break-all">~/.openclaw/clawpanel.json</code>'
|
||||
overlay.innerHTML = `
|
||||
<div class="login-card">
|
||||
${renderLoginLanguageSwitch()}
|
||||
${_logoSvg}
|
||||
<div class="login-title">ClawPanel</div>
|
||||
<div class="login-desc">${hasDefault
|
||||
@@ -218,6 +502,14 @@ function showLoginOverlay(defaultPw) {
|
||||
_hideSplash()
|
||||
|
||||
return new Promise((resolve) => {
|
||||
overlay.querySelector('#login-lang-select')?.addEventListener('change', (event) => {
|
||||
const next = event.target.value
|
||||
if (!next || next === getLang()) return
|
||||
setLang(next)
|
||||
overlay.remove()
|
||||
showLoginOverlay(defaultPw).then(resolve)
|
||||
})
|
||||
|
||||
overlay.querySelector('#login-form').addEventListener('submit', async (e) => {
|
||||
e.preventDefault()
|
||||
const pw = overlay.querySelector('#login-pw').value
|
||||
@@ -401,12 +693,17 @@ async function boot() {
|
||||
if (sessionStorage.getItem('clawpanel_must_change_pw') === '1') {
|
||||
const banner = document.createElement('div')
|
||||
banner.id = 'pw-change-banner'
|
||||
banner.style.cssText = 'position:fixed;top:0;left:0;right:0;z-index:999;background:linear-gradient(135deg,#6366f1,#8b5cf6);color:#fff;padding:10px 20px;display:flex;align-items:center;justify-content:center;gap:12px;font-size:13px;font-weight:500;box-shadow:0 2px 8px rgba(0,0,0,0.15)'
|
||||
banner.className = 'password-change-banner'
|
||||
banner.innerHTML = `
|
||||
<span>${statusIcon('warn', 14)} ${t('common.defaultPasswordBanner')}</span>
|
||||
<a href="#/security" style="color:#fff;background:rgba(255,255,255,0.2);min-height:44px;padding:0 14px;border-radius:8px;text-decoration:none;font-size:12px;font-weight:600;display:inline-flex;align-items:center;justify-content:center" onclick="document.getElementById('pw-change-banner').remove();sessionStorage.removeItem('clawpanel_must_change_pw')">${t('common.goSecurity')}</a>
|
||||
<button aria-label="${t('common.close')}" title="${t('common.close')}" onclick="this.parentElement.remove()" style="width:44px;height:44px;flex:0 0 44px;display:inline-flex;align-items:center;justify-content:center;background:none;border:none;border-radius:8px;color:rgba(255,255,255,0.7);cursor:pointer;font-size:16px;padding:0;margin-left:0">✕</button>
|
||||
<span class="password-change-text">${statusIcon('warn', 14)} ${t('common.defaultPasswordBanner')}</span>
|
||||
<a class="password-change-action" href="#/security" data-pw-banner-action>${t('common.goSecurity')}</a>
|
||||
<button class="password-change-close" type="button" aria-label="${t('common.close')}" title="${t('common.close')}" data-pw-banner-close>✕</button>
|
||||
`
|
||||
banner.querySelector('[data-pw-banner-action]')?.addEventListener('click', () => {
|
||||
banner.remove()
|
||||
sessionStorage.removeItem('clawpanel_must_change_pw')
|
||||
})
|
||||
banner.querySelector('[data-pw-banner-close]')?.addEventListener('click', () => banner.remove())
|
||||
document.body.prepend(banner)
|
||||
}
|
||||
|
||||
@@ -862,81 +1159,57 @@ function showGuardianRecovery() {
|
||||
|
||||
// === 全局版本更新检测 ===
|
||||
const UPDATE_CHECK_INTERVAL = 30 * 60 * 1000 // 30 分钟
|
||||
const ANNOUNCEMENT_CHECK_INTERVAL = 30 * 60 * 1000 // 30 分钟
|
||||
let _updateCheckTimer = null
|
||||
let _announcementCheckTimer = null
|
||||
|
||||
async function checkGlobalUpdate() {
|
||||
const banner = document.getElementById('update-banner')
|
||||
if (!banner) return
|
||||
|
||||
try {
|
||||
const info = await api.checkFrontendUpdate()
|
||||
if (!info.hasUpdate) return
|
||||
const [panelResult, versionResult] = await Promise.allSettled([
|
||||
api.checkPanelUpdate(),
|
||||
getInstalledAppVersion(),
|
||||
])
|
||||
const panelInfo = panelResult.status === 'fulfilled' ? panelResult.value : null
|
||||
const installedVersion = versionResult.status === 'fulfilled' ? versionResult.value : APP_VERSION
|
||||
|
||||
const ver = info.latestVersion || info.manifest?.version || ''
|
||||
if (!ver) return
|
||||
const panelLatest = panelInfo?.latest || ''
|
||||
const hasFullInstallerUpdate = !!panelLatest && compareVersions(panelLatest, installedVersion) > 0
|
||||
if (!hasFullInstallerUpdate) return
|
||||
|
||||
const ver = panelLatest
|
||||
|
||||
// 用户已忽略过该版本,不再打扰
|
||||
const dismissed = localStorage.getItem('clawpanel_update_dismissed')
|
||||
if (dismissed === ver) return
|
||||
if (sessionStorage.getItem(`clawpanel_update_seen_session_${ver}`) === '1') return
|
||||
|
||||
const changelog = info.manifest?.changelog || ''
|
||||
const canHotUpdate = isTauriRuntime()
|
||||
&& info.manifest?.downloadUrl
|
||||
&& info.manifest?.hash
|
||||
|
||||
banner.classList.remove('update-banner-hidden')
|
||||
banner.innerHTML = `
|
||||
<div class="update-banner-content">
|
||||
<div class="update-banner-text">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
|
||||
<span class="update-banner-ver">${t('about.versionAvailable', { version: ver })}</span>
|
||||
${changelog ? `<span class="update-banner-changelog">· ${changelog}</span>` : ''}
|
||||
</div>
|
||||
${canHotUpdate ? `<button class="btn btn-sm btn-primary" id="btn-hot-update">${t('about.hotUpdateNow')}</button>` : ''}
|
||||
<a class="btn btn-sm" href="https://claw.qt.cool" target="_blank" rel="noopener">${t('about.downloadFromWebsite')}</a>
|
||||
<a class="btn btn-sm" href="https://github.com/qingchencloud/clawpanel/releases" target="_blank" rel="noopener">${t('about.downloadFromGitHub')}</a>
|
||||
<button class="update-banner-close" id="btn-update-dismiss" title="${t('about.dismissVersion')}">✕</button>
|
||||
</div>
|
||||
`
|
||||
|
||||
// 关闭按钮:记住忽略的版本
|
||||
banner.querySelector('#btn-update-dismiss')?.addEventListener('click', () => {
|
||||
localStorage.setItem('clawpanel_update_dismissed', ver)
|
||||
banner.classList.add('update-banner-hidden')
|
||||
})
|
||||
|
||||
// 热更新按钮
|
||||
const hotUpdateBtn = banner.querySelector('#btn-hot-update')
|
||||
if (hotUpdateBtn && canHotUpdate) {
|
||||
hotUpdateBtn.addEventListener('click', async () => {
|
||||
hotUpdateBtn.disabled = true
|
||||
hotUpdateBtn.textContent = t('about.hotUpdateDownloading')
|
||||
try {
|
||||
await api.downloadFrontendUpdate(
|
||||
info.manifest.downloadUrl,
|
||||
info.manifest.hash,
|
||||
ver
|
||||
)
|
||||
hotUpdateBtn.style.display = 'none'
|
||||
toast(t('about.hotUpdateDone'), 'success')
|
||||
// 在 banner 中插入重启按钮
|
||||
const rebootBtn = document.createElement('button')
|
||||
rebootBtn.className = 'btn btn-sm btn-primary'
|
||||
rebootBtn.textContent = t('about.restartApp')
|
||||
rebootBtn.onclick = () => api.relaunchApp().catch(() => {})
|
||||
banner.querySelector('.update-banner-text').after(rebootBtn)
|
||||
} catch (err) {
|
||||
hotUpdateBtn.disabled = false
|
||||
hotUpdateBtn.textContent = t('about.hotUpdateNow')
|
||||
toast(t('about.hotUpdateFailed') + ': ' + (err.message || err), 'error')
|
||||
}
|
||||
})
|
||||
}
|
||||
showInstallerUpdateModal({ panelInfo, latest: ver, installedVersion })
|
||||
} catch {
|
||||
// 检查失败静默忽略
|
||||
}
|
||||
}
|
||||
|
||||
async function openInstallerUpdateDialog(options = {}) {
|
||||
const panelInfo = options.panelInfo || await api.checkPanelUpdate()
|
||||
const installedVersion = options.installedVersion || await getInstalledAppVersion()
|
||||
const latest = options.latest || panelInfo?.latest || ''
|
||||
if (!latest) return false
|
||||
if (!options.force && compareVersions(latest, installedVersion) <= 0) return false
|
||||
|
||||
showInstallerUpdateModal({ panelInfo, latest, installedVersion })
|
||||
return true
|
||||
}
|
||||
|
||||
window.addEventListener('clawpanel:show-installer-update', (event) => {
|
||||
openInstallerUpdateDialog({ ...(event.detail || {}), force: true }).catch(() => {
|
||||
toast(t('about.checkUpdateFailed'), 'error')
|
||||
})
|
||||
})
|
||||
|
||||
async function checkSiteAnnouncements() {
|
||||
await refreshSiteMessageCenter({ auto: true })
|
||||
}
|
||||
|
||||
function startUpdateChecker() {
|
||||
// Web 模式:浏览器每次刷新都拿最新前端,前端热更新无意义;跳过避免 404 噪音
|
||||
if (!isTauri) return
|
||||
@@ -946,6 +1219,11 @@ function startUpdateChecker() {
|
||||
_updateCheckTimer = setInterval(checkGlobalUpdate, UPDATE_CHECK_INTERVAL)
|
||||
}
|
||||
|
||||
function startAnnouncementChecker() {
|
||||
setTimeout(checkSiteAnnouncements, 8000)
|
||||
_announcementCheckTimer = setInterval(checkSiteAnnouncements, ANNOUNCEMENT_CHECK_INTERVAL)
|
||||
}
|
||||
|
||||
// 启动:先检查后端 → 认证 → 加载应用
|
||||
;(async () => {
|
||||
// Web 模式:先检测后端是否在线(不在线则显示提示,不加载应用)
|
||||
@@ -976,7 +1254,11 @@ function startUpdateChecker() {
|
||||
<div style="margin-top:24px;font-size:11px;color:#a1a1aa">${t('common.pageLoadFailedHint')}<br><a href="https://github.com/qingchencloud/clawpanel/issues" target="_blank" style="color:#6366f1">GitHub Issues</a></div>
|
||||
</div>`
|
||||
}
|
||||
initSiteMessageCenter({
|
||||
fetcher: () => api.checkSiteAnnouncements(getLang()),
|
||||
})
|
||||
startUpdateChecker()
|
||||
startAnnouncementChecker()
|
||||
|
||||
// 初始化全局 AI 助手浮动按钮(延迟加载,不阻塞启动)
|
||||
setTimeout(async () => {
|
||||
|
||||
@@ -629,53 +629,138 @@ async function doInstall(page, title, source, version) {
|
||||
}
|
||||
}
|
||||
|
||||
function escapeAttr(value) {
|
||||
return String(value || '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"')
|
||||
}
|
||||
|
||||
function safeExternalHref(raw, fallback = 'https://claw.qt.cool') {
|
||||
try {
|
||||
const url = new URL(String(raw || '').trim() || fallback, 'https://claw.qt.cool')
|
||||
const host = url.hostname.toLowerCase()
|
||||
if (host === 'claw.qt.cool') {
|
||||
url.protocol = 'https:'
|
||||
return url.toString()
|
||||
}
|
||||
if ((host === 'github.com' || host === 'api.github.com') && url.protocol === 'https:') {
|
||||
return url.toString()
|
||||
}
|
||||
} catch {}
|
||||
return fallback
|
||||
}
|
||||
|
||||
function formatBytes(value, fallback = '') {
|
||||
const size = Number(value)
|
||||
if (!Number.isFinite(size) || size <= 0) return fallback
|
||||
const units = ['B', 'KB', 'MB', 'GB']
|
||||
let next = size
|
||||
let unit = 0
|
||||
while (next >= 1024 && unit < units.length - 1) {
|
||||
next /= 1024
|
||||
unit += 1
|
||||
}
|
||||
return `${next >= 10 || unit === 0 ? next.toFixed(0) : next.toFixed(1)} ${units[unit]}`
|
||||
}
|
||||
|
||||
function formatInstallerSummary(asset) {
|
||||
if (!asset || typeof asset !== 'object') return ''
|
||||
const name = asset.name || t('about.downloadFullInstaller')
|
||||
const size = asset.sizeText || formatBytes(asset.size)
|
||||
const meta = [
|
||||
[asset.platform, asset.arch].filter(Boolean).join('/'),
|
||||
asset.fileType ? String(asset.fileType).toUpperCase() : '',
|
||||
size,
|
||||
].filter(Boolean).join(' · ')
|
||||
return meta ? `${name} (${meta})` : name
|
||||
}
|
||||
|
||||
function renderPanelUpdateMeta({ statusHtml, installerSummary, websiteDownloadUrl, githubFallbackUrl }) {
|
||||
return `
|
||||
<div class="panel-update-status">${statusHtml}</div>
|
||||
${installerSummary ? `<div class="panel-update-installer">${escapeAttr(installerSummary)}</div>` : ''}
|
||||
<div class="panel-update-actions">
|
||||
<button class="btn btn-secondary btn-sm" type="button" data-panel-update-details>${t('about.viewUpdateDetails')}</button>
|
||||
<a class="btn btn-primary btn-sm" href="${escapeAttr(websiteDownloadUrl)}" target="_blank" rel="noopener">${t('about.downloadRecommendedInstaller')}</a>
|
||||
<a class="btn btn-secondary btn-sm" href="${escapeAttr(githubFallbackUrl)}" target="_blank" rel="noopener">${t('about.downloadFromGitHub')}</a>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
function bindPanelUpdateDetails(meta, detail) {
|
||||
meta.querySelector('[data-panel-update-details]')?.addEventListener('click', () => {
|
||||
window.dispatchEvent(new CustomEvent('clawpanel:show-installer-update', {
|
||||
detail: { ...detail, force: true },
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
async function checkNewVersion(cards, panelVersion) {
|
||||
const el = () => cards.querySelector('#panel-update-meta')
|
||||
const btnSm = 'padding:2px 8px;font-size:var(--font-size-xs)'
|
||||
|
||||
// 尝试获取 Tauri 二进制版本,检测「假更新」:
|
||||
// 前端通过热更新升级到 v0.13.0,但 Tauri 二进制仍是 v0.9.9
|
||||
// Tauri 二进制版本才是完整安装包版本;前端版本可能来自历史热更新目录。
|
||||
let binaryVersion = panelVersion
|
||||
try {
|
||||
const { getVersion } = await import('@tauri-apps/api/app')
|
||||
binaryVersion = await getVersion()
|
||||
} catch {}
|
||||
|
||||
// 前端版本 > 二进制版本 = 热更新导致版本不一致
|
||||
// 前端版本 > 二进制版本时,提醒用户用完整安装包覆盖到真实新版本。
|
||||
const isFakeUpdate = binaryVersion !== panelVersion && compareVersions(panelVersion, binaryVersion) > 0
|
||||
|
||||
try {
|
||||
const info = await api.checkPanelUpdate()
|
||||
const meta = el()
|
||||
if (!meta) return
|
||||
meta.classList.add('panel-update-meta')
|
||||
meta.removeAttribute('style')
|
||||
|
||||
const latest = info?.latest || ''
|
||||
const recommendedAsset = info?.recommendedAsset || null
|
||||
const installerSummary = formatInstallerSummary(recommendedAsset)
|
||||
const websiteDownloadUrl = safeExternalHref(info?.recommendedAsset?.downloadUrl || info?.downloadUrl)
|
||||
const githubFallbackUrl = info?.source === 'site'
|
||||
? 'https://github.com/qingchencloud/clawpanel/releases'
|
||||
: safeExternalHref(info?.url, 'https://github.com/qingchencloud/clawpanel/releases')
|
||||
// 用二进制版本(真实应用版本)做比较,避免假更新导致误判为「已是最新」
|
||||
const effectiveVersion = isFakeUpdate ? binaryVersion : panelVersion
|
||||
|
||||
if (isFakeUpdate) {
|
||||
meta.innerHTML = `
|
||||
<span style="color:var(--warning)">⚠️ ${t('about.versionMismatch', { frontend: panelVersion, binary: binaryVersion })}</span>
|
||||
<span style="color:var(--text-tertiary);font-size:var(--font-size-xs)">${t('about.hotUpdateDeprecated')}</span>
|
||||
<a class="btn btn-primary btn-sm" href="https://claw.qt.cool" target="_blank" rel="noopener" style="${btnSm}">${t('about.downloadFullInstaller')}</a>
|
||||
<a class="btn btn-secondary btn-sm" href="${info.url || 'https://github.com/qingchencloud/clawpanel/releases'}" target="_blank" rel="noopener" style="${btnSm}">${t('about.downloadFromGitHub')}</a>
|
||||
`
|
||||
meta.innerHTML = renderPanelUpdateMeta({
|
||||
statusHtml: `<span class="panel-update-warning">⚠️ ${t('about.versionMismatch', { frontend: panelVersion, binary: binaryVersion })}</span><span>${t('about.installerOnlyUpdateHint')}</span>`,
|
||||
installerSummary,
|
||||
websiteDownloadUrl,
|
||||
githubFallbackUrl,
|
||||
})
|
||||
bindPanelUpdateDetails(meta, { panelInfo: info, latest, installedVersion: binaryVersion })
|
||||
} else if (latest && latest !== effectiveVersion && compareVersions(latest, effectiveVersion) > 0) {
|
||||
meta.innerHTML = `
|
||||
<span style="color:var(--accent)">${t('about.newVersionAvailable', { version: latest })}</span>
|
||||
<a class="btn btn-primary btn-sm" href="https://claw.qt.cool" target="_blank" rel="noopener" style="${btnSm}">${t('about.downloadFromWebsite')}</a>
|
||||
<a class="btn btn-secondary btn-sm" href="${info.url || 'https://github.com/qingchencloud/clawpanel/releases'}" target="_blank" rel="noopener" style="${btnSm}">${t('about.downloadFromGitHub')}</a>
|
||||
`
|
||||
meta.innerHTML = renderPanelUpdateMeta({
|
||||
statusHtml: `<span class="panel-update-new">${t('about.newVersionAvailable', { version: latest })}</span>`,
|
||||
installerSummary,
|
||||
websiteDownloadUrl,
|
||||
githubFallbackUrl,
|
||||
})
|
||||
bindPanelUpdateDetails(meta, { panelInfo: info, latest, installedVersion: binaryVersion })
|
||||
} else {
|
||||
meta.innerHTML = `<span style="color:var(--success)">${t('about.upToDate')}</span>`
|
||||
}
|
||||
} catch (err) {
|
||||
const meta = el()
|
||||
if (!meta) return
|
||||
meta.classList.add('panel-update-meta')
|
||||
meta.removeAttribute('style')
|
||||
if (isFakeUpdate) {
|
||||
meta.innerHTML = `<span style="color:var(--warning)">⚠️ ${t('about.versionMismatch', { frontend: panelVersion, binary: binaryVersion })}</span> <a class="btn btn-primary btn-sm" href="https://claw.qt.cool" target="_blank" rel="noopener" style="${btnSm}">${t('about.downloadFullInstaller')}</a>`
|
||||
meta.innerHTML = `
|
||||
<div class="panel-update-status"><span class="panel-update-warning">⚠️ ${t('about.versionMismatch', { frontend: panelVersion, binary: binaryVersion })}</span></div>
|
||||
<div class="panel-update-actions">
|
||||
<a class="btn btn-primary btn-sm" href="https://claw.qt.cool" target="_blank" rel="noopener">${t('about.downloadFullInstaller')}</a>
|
||||
</div>
|
||||
`
|
||||
} else {
|
||||
meta.innerHTML = `<span style="color:var(--text-tertiary)">${t('about.checkUpdateFailed')}</span> <a class="btn btn-secondary btn-sm" href="https://claw.qt.cool" target="_blank" rel="noopener" style="${btnSm}">${t('about.goToWebsite')}</a>`
|
||||
meta.innerHTML = `
|
||||
<div class="panel-update-status"><span>${t('about.checkUpdateFailed')}</span></div>
|
||||
<div class="panel-update-actions">
|
||||
<a class="btn btn-secondary btn-sm" href="https://claw.qt.cool" target="_blank" rel="noopener">${t('about.goToWebsite')}</a>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -239,7 +239,9 @@ async function _loadDashboardDataInner(page, fullRefresh, loadSeq) {
|
||||
}
|
||||
// 每个请求独立超时:避免单个慢请求拖垮整体渲染
|
||||
const coreP = Promise.allSettled([
|
||||
withTimeout(api.getServicesStatus(), 2500),
|
||||
// Windows 后端在端口未监听时最多会做 1s + 300ms + 2s 的 TCP 检测;
|
||||
// 这里留出余量,避免 Gateway 停止时被误报为“服务状态加载失败”。
|
||||
withTimeout(api.getServicesStatus(), 4500),
|
||||
withTimeout(api.readOpenclawConfig(), 2000),
|
||||
withTimeout(api.readPanelConfig(), 2000),
|
||||
])
|
||||
|
||||
@@ -99,6 +99,53 @@
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.panel-update-meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.panel-update-status {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.panel-update-warning {
|
||||
color: var(--warning);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.panel-update-new {
|
||||
color: var(--accent);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.panel-update-installer {
|
||||
max-width: 100%;
|
||||
color: var(--text-tertiary);
|
||||
font-size: var(--font-size-xs);
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.panel-update-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.panel-update-actions .btn {
|
||||
padding: 2px 8px;
|
||||
font-size: var(--font-size-xs);
|
||||
}
|
||||
|
||||
/* 状态点 */
|
||||
.status-dot {
|
||||
width: 8px;
|
||||
@@ -871,13 +918,43 @@ mark {
|
||||
}
|
||||
#login-overlay.hide { opacity: 0; pointer-events: none; }
|
||||
.login-card {
|
||||
width: 360px; max-width: 90vw; padding: 40px 32px;
|
||||
position: relative;
|
||||
width: 360px; max-width: 90vw; padding: 54px 32px 40px;
|
||||
background: var(--bg-card, #fff);
|
||||
border: 1px solid var(--border, #e4e4e7);
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 8px 32px rgba(0,0,0,0.08);
|
||||
text-align: center;
|
||||
}
|
||||
.login-lang-switch {
|
||||
position: absolute;
|
||||
top: 14px;
|
||||
right: 14px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
min-height: 32px;
|
||||
padding: 4px 8px 4px 10px;
|
||||
border: 1px solid var(--border-primary, #e4e4e7);
|
||||
border-radius: 999px;
|
||||
background: color-mix(in srgb, var(--bg-secondary, #f4f4f5) 86%, transparent);
|
||||
color: var(--text-tertiary, #a1a1aa);
|
||||
font-size: 11px;
|
||||
font-weight: 650;
|
||||
}
|
||||
.login-lang-switch select {
|
||||
max-width: 150px;
|
||||
border: 0;
|
||||
outline: none;
|
||||
background: transparent;
|
||||
color: var(--text-secondary, #52525b);
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
.login-lang-switch:focus-within {
|
||||
border-color: var(--primary, #6366f1);
|
||||
box-shadow: 0 0 0 3px rgba(99,102,241,0.12);
|
||||
}
|
||||
.login-card .login-logo {
|
||||
width: 48px; height: 48px; margin: 0 auto 16px;
|
||||
color: var(--primary, #6366f1);
|
||||
|
||||
@@ -37,12 +37,6 @@
|
||||
#sidebar.sidebar-collapsed .sidebar-footer {
|
||||
padding: var(--space-sm) 6px;
|
||||
}
|
||||
#sidebar.sidebar-collapsed #btn-theme-toggle {
|
||||
justify-content: center;
|
||||
}
|
||||
#sidebar.sidebar-collapsed #btn-theme-toggle span {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
padding: var(--space-lg) var(--space-lg);
|
||||
@@ -204,10 +198,108 @@
|
||||
}
|
||||
|
||||
.sidebar-footer {
|
||||
padding: var(--space-sm);
|
||||
position: relative;
|
||||
padding: 10px 12px 8px;
|
||||
border-top: 1px solid var(--border-secondary);
|
||||
}
|
||||
|
||||
.sidebar-tools {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin: 0 0 8px;
|
||||
padding: 0 2px;
|
||||
}
|
||||
|
||||
.sidebar-tools > .lang-switcher {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
min-width: 32px;
|
||||
min-height: 32px;
|
||||
max-width: 32px;
|
||||
max-height: 32px;
|
||||
flex: 0 0 32px;
|
||||
display: inline-grid;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
.sidebar-tools > .sidebar-tool-btn,
|
||||
.sidebar-tools > .lang-switcher > .sidebar-tool-btn {
|
||||
position: relative;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
min-width: 32px;
|
||||
min-height: 32px;
|
||||
max-width: 32px;
|
||||
max-height: 32px;
|
||||
flex: 0 0 32px;
|
||||
display: inline-grid;
|
||||
place-items: center;
|
||||
padding: 0;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 9px;
|
||||
background: transparent;
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
line-height: 1;
|
||||
appearance: none;
|
||||
box-shadow: none;
|
||||
transition: background 0.16s ease, border-color 0.16s ease, color 0.16s ease;
|
||||
}
|
||||
|
||||
.sidebar-tools > .sidebar-tool-btn:hover,
|
||||
.sidebar-tools > .lang-switcher > .sidebar-tool-btn:hover {
|
||||
border-color: var(--border-primary);
|
||||
background: color-mix(in srgb, var(--primary) 7%, transparent);
|
||||
color: var(--primary);
|
||||
box-shadow: none;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.sidebar-tools > .sidebar-tool-btn:focus-visible,
|
||||
.sidebar-tools > .lang-switcher > .sidebar-tool-btn:focus-visible {
|
||||
outline: 2px solid color-mix(in srgb, var(--primary) 58%, transparent);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.sidebar-tools > .sidebar-tool-btn svg,
|
||||
.sidebar-tools > .lang-switcher > .sidebar-tool-btn svg {
|
||||
width: 17px;
|
||||
height: 17px;
|
||||
min-width: 17px;
|
||||
min-height: 17px;
|
||||
display: block;
|
||||
flex: 0 0 auto;
|
||||
fill: none;
|
||||
stroke: currentColor;
|
||||
stroke-width: 2;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
|
||||
.site-message-tool-badge {
|
||||
position: absolute;
|
||||
top: -4px;
|
||||
right: -4px;
|
||||
min-width: 15px;
|
||||
height: 15px;
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 4px;
|
||||
border-radius: 999px;
|
||||
border: 2px solid var(--bg-secondary);
|
||||
background: var(--error);
|
||||
color: #fff;
|
||||
font-size: 9px;
|
||||
font-weight: 750;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.site-message-trigger.has-unread .site-message-tool-badge {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
/* === 内核可升级提示卡(侧边栏 footer 之上) === */
|
||||
.kernel-upgrade-hint {
|
||||
display: flex;
|
||||
@@ -327,19 +419,10 @@
|
||||
|
||||
/* Language switcher */
|
||||
.lang-switcher {
|
||||
position: relative;
|
||||
position: static;
|
||||
}
|
||||
.lang-trigger {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
cursor: pointer;
|
||||
background: none;
|
||||
border: none;
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
text-align: left;
|
||||
}
|
||||
.lang-trigger span {
|
||||
flex: 1;
|
||||
@@ -358,13 +441,13 @@
|
||||
}
|
||||
.lang-dropdown {
|
||||
position: absolute;
|
||||
bottom: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: calc(100% - 4px);
|
||||
left: 12px;
|
||||
right: 12px;
|
||||
background: var(--bg-primary);
|
||||
border: 1px solid var(--border-primary);
|
||||
border-radius: var(--radius-md);
|
||||
box-shadow: 0 -4px 16px rgba(0, 0, 0, 0.15);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 -12px 30px rgba(15, 23, 42, 0.16);
|
||||
max-height: 0;
|
||||
overflow: hidden;
|
||||
opacity: 0;
|
||||
@@ -432,13 +515,31 @@
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Collapsed sidebar: hide lang label, keep icon */
|
||||
#sidebar.sidebar-collapsed .lang-trigger span,
|
||||
#sidebar.sidebar-collapsed .lang-chevron {
|
||||
display: none;
|
||||
/* Collapsed sidebar: stack tool icons */
|
||||
#sidebar.sidebar-collapsed .sidebar-tools {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 0;
|
||||
}
|
||||
#sidebar.sidebar-collapsed .lang-trigger {
|
||||
justify-content: center;
|
||||
#sidebar.sidebar-collapsed .sidebar-tools > .sidebar-tool-btn,
|
||||
#sidebar.sidebar-collapsed .sidebar-tools > .lang-switcher > .sidebar-tool-btn {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
min-width: 34px;
|
||||
min-height: 34px;
|
||||
max-width: 34px;
|
||||
max-height: 34px;
|
||||
flex-basis: 34px;
|
||||
}
|
||||
#sidebar.sidebar-collapsed .sidebar-tools > .lang-switcher {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
min-width: 34px;
|
||||
min-height: 34px;
|
||||
max-width: 34px;
|
||||
max-height: 34px;
|
||||
flex-basis: 34px;
|
||||
}
|
||||
#sidebar.sidebar-collapsed .lang-dropdown {
|
||||
left: 100%;
|
||||
@@ -595,65 +696,266 @@
|
||||
background: var(--accent-muted);
|
||||
}
|
||||
|
||||
/* 版本更新通知横幅 */
|
||||
.update-banner {
|
||||
background: linear-gradient(135deg, #6366f1, #8b5cf6);
|
||||
color: #fff;
|
||||
padding: 10px 20px;
|
||||
font-size: var(--font-size-sm);
|
||||
z-index: 100;
|
||||
transition: all 300ms ease;
|
||||
/* 完整安装包更新弹窗 */
|
||||
.modal-overlay.installer-update-overlay {
|
||||
padding: var(--space-lg);
|
||||
overflow: hidden;
|
||||
max-height: 60px;
|
||||
}
|
||||
.update-banner-hidden {
|
||||
max-height: 0;
|
||||
padding: 0 20px;
|
||||
opacity: 0;
|
||||
.modal.installer-update-modal {
|
||||
--installer-update-modal-pad: 22px;
|
||||
position: relative;
|
||||
width: min(560px, calc(100vw - 32px));
|
||||
max-width: 560px;
|
||||
min-width: 0;
|
||||
max-height: calc(100vh - 48px);
|
||||
padding: var(--installer-update-modal-pad);
|
||||
gap: var(--space-lg);
|
||||
overflow: hidden;
|
||||
}
|
||||
.update-banner-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
.modal.installer-update-modal:focus {
|
||||
outline: none;
|
||||
}
|
||||
.update-banner-text {
|
||||
flex: 1;
|
||||
min-width: 200px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.update-banner-ver {
|
||||
font-weight: 700;
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
.update-banner-changelog {
|
||||
font-size: var(--font-size-xs);
|
||||
opacity: 0.85;
|
||||
}
|
||||
.update-banner .btn {
|
||||
background: rgba(255,255,255,0.2);
|
||||
border: 1px solid rgba(255,255,255,0.3);
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
.update-banner .btn:hover {
|
||||
background: rgba(255,255,255,0.3);
|
||||
}
|
||||
.update-banner-close {
|
||||
background: none;
|
||||
border: none;
|
||||
color: rgba(255,255,255,0.7);
|
||||
.installer-update-close {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
right: 12px;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
display: inline-grid;
|
||||
place-items: center;
|
||||
border: 0;
|
||||
border-radius: var(--radius-md);
|
||||
background: transparent;
|
||||
color: var(--text-tertiary);
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
padding: 0 4px;
|
||||
font-size: 20px;
|
||||
line-height: 1;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.update-banner-close:hover {
|
||||
color: #fff;
|
||||
.installer-update-close:hover {
|
||||
background: var(--bg-glass-hover);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
.installer-update-head {
|
||||
display: flex;
|
||||
gap: var(--space-md);
|
||||
align-items: flex-start;
|
||||
padding-right: 32px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.installer-update-icon {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
flex-shrink: 0;
|
||||
border-radius: var(--radius-lg);
|
||||
background: var(--accent-muted);
|
||||
color: var(--accent);
|
||||
}
|
||||
.installer-update-icon svg {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
}
|
||||
.installer-update-title-wrap {
|
||||
min-width: 0;
|
||||
}
|
||||
.installer-update-kicker {
|
||||
color: var(--accent);
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: 700;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
.installer-update-title {
|
||||
color: var(--text-primary);
|
||||
font-size: var(--font-size-xl);
|
||||
font-weight: 700;
|
||||
line-height: 1.25;
|
||||
}
|
||||
.installer-update-subtitle {
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--font-size-sm);
|
||||
line-height: 1.6;
|
||||
margin-top: 6px;
|
||||
}
|
||||
.installer-update-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: var(--space-sm);
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.installer-update-version-card,
|
||||
.installer-update-asset,
|
||||
.installer-update-steps,
|
||||
.installer-update-notes {
|
||||
border: 1px solid var(--border-primary);
|
||||
border-radius: var(--radius-lg);
|
||||
background: var(--bg-card);
|
||||
}
|
||||
.installer-update-version-card {
|
||||
padding: var(--space-md);
|
||||
}
|
||||
.installer-update-version-card span,
|
||||
.installer-update-asset-label,
|
||||
.installer-update-section-title {
|
||||
display: block;
|
||||
color: var(--text-tertiary);
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: 600;
|
||||
}
|
||||
.installer-update-version-card strong {
|
||||
display: block;
|
||||
color: var(--text-primary);
|
||||
font-size: var(--font-size-lg);
|
||||
margin-top: 4px;
|
||||
}
|
||||
.installer-update-version-card-new {
|
||||
border-color: rgba(99, 102, 241, 0.35);
|
||||
background: var(--accent-muted);
|
||||
}
|
||||
.installer-update-asset,
|
||||
.installer-update-steps,
|
||||
.installer-update-notes {
|
||||
padding: var(--space-md);
|
||||
}
|
||||
.installer-update-asset,
|
||||
.installer-update-steps {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.installer-update-notes {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
.installer-update-asset-name {
|
||||
color: var(--text-primary);
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 650;
|
||||
margin-top: 6px;
|
||||
word-break: break-word;
|
||||
}
|
||||
.installer-update-asset-meta,
|
||||
.installer-update-notes {
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--font-size-xs);
|
||||
line-height: 1.6;
|
||||
}
|
||||
.installer-update-asset-meta {
|
||||
margin-top: 4px;
|
||||
}
|
||||
.installer-update-release-text {
|
||||
flex: 1 1 auto;
|
||||
min-height: 150px;
|
||||
max-height: clamp(170px, 28vh, 320px);
|
||||
margin-top: 10px;
|
||||
overflow: auto;
|
||||
padding-right: 6px;
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
}
|
||||
.installer-update-release-markdown {
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--font-size-xs);
|
||||
line-height: 1.65;
|
||||
}
|
||||
.installer-update-release-markdown > *:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
.installer-update-release-markdown > *:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.installer-update-release-markdown h1,
|
||||
.installer-update-release-markdown h2,
|
||||
.installer-update-release-markdown h3,
|
||||
.installer-update-release-markdown h4 {
|
||||
color: var(--text-primary);
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 650;
|
||||
margin: 12px 0 6px;
|
||||
}
|
||||
.installer-update-release-markdown p {
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
.installer-update-release-markdown ul,
|
||||
.installer-update-release-markdown ol {
|
||||
margin: 6px 0 10px;
|
||||
padding-left: 20px;
|
||||
}
|
||||
.installer-update-release-markdown li {
|
||||
margin: 3px 0;
|
||||
}
|
||||
.installer-update-release-markdown a {
|
||||
color: var(--accent);
|
||||
text-decoration: none;
|
||||
}
|
||||
.installer-update-release-markdown a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.installer-update-release-markdown code {
|
||||
padding: 1px 5px;
|
||||
border: 1px solid var(--border-primary);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--text-primary);
|
||||
font-size: 0.92em;
|
||||
}
|
||||
.installer-update-release-markdown pre {
|
||||
margin: 8px 0;
|
||||
padding: 10px;
|
||||
border: 1px solid var(--border-primary);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--bg-tertiary);
|
||||
overflow: auto;
|
||||
}
|
||||
.installer-update-release-markdown pre code {
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
}
|
||||
.installer-update-release-markdown table {
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin: 8px 0 10px;
|
||||
border-collapse: collapse;
|
||||
overflow: auto;
|
||||
}
|
||||
.installer-update-release-markdown th,
|
||||
.installer-update-release-markdown td {
|
||||
padding: 6px 8px;
|
||||
border: 1px solid var(--border-primary);
|
||||
text-align: left;
|
||||
vertical-align: top;
|
||||
}
|
||||
.installer-update-release-markdown th {
|
||||
color: var(--text-primary);
|
||||
font-weight: 650;
|
||||
background: var(--bg-tertiary);
|
||||
}
|
||||
.installer-update-steps ol {
|
||||
margin: 8px 0 0;
|
||||
padding-left: 20px;
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--font-size-sm);
|
||||
line-height: 1.7;
|
||||
}
|
||||
.installer-update-actions {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
gap: var(--space-sm);
|
||||
flex-wrap: wrap;
|
||||
margin: 0 calc(var(--installer-update-modal-pad) * -1) calc(var(--installer-update-modal-pad) * -1);
|
||||
padding: 14px var(--installer-update-modal-pad) var(--installer-update-modal-pad);
|
||||
border-top: 1px solid var(--border-primary);
|
||||
background: var(--bg-secondary);
|
||||
box-shadow: 0 -16px 24px rgba(15, 23, 42, 0.08);
|
||||
}
|
||||
.modal.installer-update-modal .installer-update-actions .btn {
|
||||
min-height: 36px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* Gateway 状态条:低调信息提示,避免高对比琥珀色造成焦虑 */
|
||||
@@ -735,6 +1037,86 @@
|
||||
background: var(--bg-glass) !important;
|
||||
}
|
||||
|
||||
/* 默认密码提醒:轻量安全提示,避免压住主界面 */
|
||||
.password-change-banner {
|
||||
position: fixed;
|
||||
top: 8px;
|
||||
left: 50%;
|
||||
z-index: 999;
|
||||
width: min(620px, calc(100vw - 24px));
|
||||
min-height: 38px;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto auto;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 8px 6px 12px;
|
||||
border: 1px solid color-mix(in srgb, var(--warning) 30%, var(--border-primary));
|
||||
border-radius: 12px;
|
||||
background: color-mix(in srgb, var(--bg-secondary) 92%, var(--warning));
|
||||
color: var(--text-primary);
|
||||
box-shadow: 0 10px 30px rgba(15, 23, 42, 0.14);
|
||||
transform: translateX(-50%);
|
||||
pointer-events: none;
|
||||
}
|
||||
.password-change-text {
|
||||
min-width: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
color: var(--text-primary);
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: 650;
|
||||
line-height: 1.35;
|
||||
}
|
||||
.password-change-text svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
flex: 0 0 14px;
|
||||
stroke: var(--warning) !important;
|
||||
}
|
||||
.password-change-action,
|
||||
.password-change-close {
|
||||
pointer-events: auto;
|
||||
}
|
||||
.password-change-action {
|
||||
min-height: 28px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 10px;
|
||||
border: 1px solid color-mix(in srgb, var(--accent) 24%, transparent);
|
||||
border-radius: 8px;
|
||||
background: color-mix(in srgb, var(--accent) 10%, transparent);
|
||||
color: var(--accent);
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: 650;
|
||||
line-height: 1;
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.password-change-action:hover {
|
||||
background: color-mix(in srgb, var(--accent) 15%, transparent);
|
||||
text-decoration: none;
|
||||
}
|
||||
.password-change-close {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
display: inline-grid;
|
||||
place-items: center;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
border-radius: 8px;
|
||||
background: transparent;
|
||||
color: var(--text-tertiary);
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
}
|
||||
.password-change-close:hover {
|
||||
background: var(--bg-glass-hover);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* === 移动端顶栏 + 侧边栏 === */
|
||||
.mobile-topbar {
|
||||
display: none;
|
||||
@@ -815,6 +1197,31 @@
|
||||
|
||||
/* === 移动端响应式 (≤768px) === */
|
||||
@media (max-width: 768px) {
|
||||
.password-change-banner {
|
||||
top: 8px;
|
||||
width: calc(100vw - 16px);
|
||||
min-height: 36px;
|
||||
gap: 6px;
|
||||
padding: 6px 6px 6px 9px;
|
||||
border-radius: 11px;
|
||||
}
|
||||
.password-change-text {
|
||||
gap: 6px;
|
||||
font-size: 11px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
.password-change-action {
|
||||
min-height: 26px;
|
||||
padding: 0 8px;
|
||||
border-radius: 7px;
|
||||
font-size: 11px;
|
||||
}
|
||||
.password-change-close {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
border-radius: 7px;
|
||||
font-size: 13px;
|
||||
}
|
||||
.mobile-topbar {
|
||||
display: flex;
|
||||
}
|
||||
@@ -862,12 +1269,48 @@
|
||||
font-size: 12px;
|
||||
gap: 6px;
|
||||
}
|
||||
.update-banner-content {
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
.modal-overlay.installer-update-overlay {
|
||||
align-items: center;
|
||||
padding: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.update-banner-text {
|
||||
min-width: auto;
|
||||
.modal.installer-update-modal {
|
||||
--installer-update-modal-pad: 18px;
|
||||
width: 100%;
|
||||
max-height: calc(100vh - 24px);
|
||||
padding: var(--installer-update-modal-pad);
|
||||
gap: 12px;
|
||||
margin: 0;
|
||||
}
|
||||
.installer-update-head {
|
||||
padding-right: 26px;
|
||||
}
|
||||
.installer-update-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.installer-update-version-card,
|
||||
.installer-update-asset,
|
||||
.installer-update-steps,
|
||||
.installer-update-notes {
|
||||
padding: 12px;
|
||||
}
|
||||
.installer-update-steps ol {
|
||||
line-height: 1.5;
|
||||
}
|
||||
.installer-update-actions {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
padding-top: 12px;
|
||||
}
|
||||
.installer-update-release-text {
|
||||
min-height: 90px;
|
||||
max-height: clamp(100px, 18vh, 170px);
|
||||
}
|
||||
.modal.installer-update-modal .installer-update-actions .btn {
|
||||
width: 100%;
|
||||
min-height: 44px;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
517
src/style/site-message-center.css
Normal file
517
src/style/site-message-center.css
Normal file
@@ -0,0 +1,517 @@
|
||||
/* 官网消息中心:通知流 + 固定系统公告 */
|
||||
.site-message-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 9100;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
background: rgba(17, 24, 39, 0.50);
|
||||
backdrop-filter: blur(3px);
|
||||
}
|
||||
.site-message-modal {
|
||||
width: min(640px, calc(100vw - 28px));
|
||||
max-height: min(560px, calc(100vh - 40px));
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: 1px solid color-mix(in srgb, var(--border-primary) 82%, transparent);
|
||||
border-radius: 14px;
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text-primary);
|
||||
box-shadow: 0 18px 48px rgba(15, 23, 42, 0.28);
|
||||
overflow: hidden;
|
||||
}
|
||||
.site-message-modal:focus {
|
||||
outline: none;
|
||||
}
|
||||
.site-message-header {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(180px, 1fr) auto auto;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 14px 16px 12px;
|
||||
border-bottom: 1px solid var(--border-secondary);
|
||||
background:
|
||||
linear-gradient(180deg, color-mix(in srgb, var(--accent) 5%, transparent), transparent 72%),
|
||||
var(--bg-secondary);
|
||||
}
|
||||
.site-message-title {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
.site-message-title-icon {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
display: inline-grid;
|
||||
place-items: center;
|
||||
flex: 0 0 auto;
|
||||
border-radius: 10px;
|
||||
background: color-mix(in srgb, var(--accent) 13%, var(--bg-tertiary));
|
||||
color: var(--accent);
|
||||
}
|
||||
.site-message-title-icon svg {
|
||||
width: 17px;
|
||||
height: 17px;
|
||||
fill: none;
|
||||
stroke: currentColor;
|
||||
stroke-width: 2;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
.site-message-header h2 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 740;
|
||||
letter-spacing: 0;
|
||||
line-height: 1.2;
|
||||
}
|
||||
.site-message-title p {
|
||||
margin: 3px 0 0;
|
||||
color: var(--text-tertiary);
|
||||
font-size: var(--font-size-xs);
|
||||
line-height: 1.35;
|
||||
}
|
||||
.site-message-tabs {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 3px;
|
||||
border: 1px solid var(--border-secondary);
|
||||
border-radius: 12px;
|
||||
background: color-mix(in srgb, var(--bg-primary) 86%, var(--bg-secondary));
|
||||
}
|
||||
.site-message-tab {
|
||||
min-height: 34px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
padding: 0 10px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 9px;
|
||||
background: transparent;
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
touch-action: manipulation;
|
||||
transition: background 150ms ease, color 150ms ease, border-color 150ms ease, box-shadow 150ms ease;
|
||||
}
|
||||
.site-message-tab:hover {
|
||||
background: var(--bg-glass-hover);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
.site-message-tab:focus-visible,
|
||||
.site-message-close:focus-visible {
|
||||
outline: 2px solid color-mix(in srgb, var(--accent) 58%, transparent);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
.site-message-tab svg,
|
||||
.site-message-close svg,
|
||||
.site-message-empty-icon svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
fill: none;
|
||||
stroke: currentColor;
|
||||
stroke-width: 2;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
.site-message-tab small {
|
||||
min-width: 17px;
|
||||
height: 17px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 5px;
|
||||
border-radius: 999px;
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--text-tertiary);
|
||||
font-size: 10px;
|
||||
font-weight: 750;
|
||||
line-height: 1;
|
||||
}
|
||||
.site-message-tab.active {
|
||||
background: color-mix(in srgb, var(--accent) 12%, var(--bg-secondary));
|
||||
color: var(--accent);
|
||||
border-color: color-mix(in srgb, var(--accent) 28%, transparent);
|
||||
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent) 9%, transparent);
|
||||
}
|
||||
.site-message-tab.active small {
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
}
|
||||
.site-message-close {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
display: inline-grid;
|
||||
place-items: center;
|
||||
border: 0;
|
||||
border-radius: 9px;
|
||||
background: transparent;
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
touch-action: manipulation;
|
||||
}
|
||||
.site-message-close:hover {
|
||||
background: var(--bg-glass-hover);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
.site-message-body {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
padding: 14px 16px 16px;
|
||||
}
|
||||
.site-message-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
padding: 10px 16px 14px;
|
||||
border-top: 1px solid var(--border-secondary);
|
||||
background: color-mix(in srgb, var(--bg-secondary) 95%, var(--bg-primary));
|
||||
}
|
||||
.site-message-footer .btn {
|
||||
min-height: 36px;
|
||||
min-width: 96px;
|
||||
}
|
||||
|
||||
.site-message-empty {
|
||||
min-height: 170px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
align-content: center;
|
||||
gap: 7px;
|
||||
padding: 22px;
|
||||
border: 1px dashed var(--border-primary);
|
||||
border-radius: 12px;
|
||||
background: color-mix(in srgb, var(--bg-card) 84%, var(--bg-primary));
|
||||
color: var(--text-tertiary);
|
||||
text-align: center;
|
||||
}
|
||||
.site-message-empty-icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
display: inline-grid;
|
||||
place-items: center;
|
||||
border-radius: 10px;
|
||||
background: color-mix(in srgb, var(--accent) 10%, var(--bg-tertiary));
|
||||
color: var(--accent);
|
||||
}
|
||||
.site-message-empty strong {
|
||||
color: var(--text-primary);
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 720;
|
||||
}
|
||||
.site-message-empty p {
|
||||
max-width: 320px;
|
||||
margin: 0;
|
||||
color: var(--text-tertiary);
|
||||
font-size: var(--font-size-sm);
|
||||
line-height: 1.55;
|
||||
}
|
||||
.site-message-section-head {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin: 0 0 10px;
|
||||
padding: 0 2px;
|
||||
}
|
||||
.site-message-section-head span {
|
||||
color: var(--text-primary);
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 740;
|
||||
}
|
||||
.site-message-section-head small {
|
||||
color: var(--text-tertiary);
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: 600;
|
||||
}
|
||||
.site-message-timeline {
|
||||
max-width: 640px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
}
|
||||
.site-message-timeline-item {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: 16px minmax(0, 1fr);
|
||||
column-gap: 12px;
|
||||
padding: 0 0 10px;
|
||||
}
|
||||
.site-message-dot {
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
margin-top: 16px;
|
||||
border-radius: 50%;
|
||||
background: var(--text-quaternary, #c7ccd4);
|
||||
box-shadow: 0 0 0 4px var(--bg-secondary);
|
||||
}
|
||||
.site-message-line {
|
||||
position: absolute;
|
||||
left: 4px;
|
||||
top: 30px;
|
||||
bottom: -1px;
|
||||
width: 1px;
|
||||
background: var(--border-secondary);
|
||||
}
|
||||
.site-message-timeline-item:last-child .site-message-line {
|
||||
display: none;
|
||||
}
|
||||
.site-message-timeline-item.level-success .site-message-dot,
|
||||
.site-message-timeline-item.level-info .site-message-dot {
|
||||
background: var(--success);
|
||||
}
|
||||
.site-message-timeline-item.level-warning .site-message-dot {
|
||||
background: var(--warning);
|
||||
}
|
||||
.site-message-timeline-item.level-critical .site-message-dot {
|
||||
background: var(--error);
|
||||
}
|
||||
.site-message-timeline-content {
|
||||
min-width: 0;
|
||||
padding: 12px 14px;
|
||||
border: 1px solid var(--border-secondary);
|
||||
border-radius: 10px;
|
||||
background: var(--bg-card);
|
||||
box-shadow: 0 1px 0 rgba(15, 23, 42, 0.03);
|
||||
}
|
||||
.site-message-item-top {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
}
|
||||
.site-message-timeline-content h3 {
|
||||
min-width: 0;
|
||||
margin: 0;
|
||||
color: var(--text-primary);
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 700;
|
||||
line-height: 1.55;
|
||||
}
|
||||
.site-message-item-top span {
|
||||
flex: 0 0 auto;
|
||||
padding: 3px 7px;
|
||||
border-radius: 999px;
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--text-tertiary);
|
||||
font-size: 10px;
|
||||
line-height: 1;
|
||||
font-weight: 700;
|
||||
}
|
||||
.site-message-timeline-content p {
|
||||
margin: 6px 0 0;
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--font-size-sm);
|
||||
line-height: 1.55;
|
||||
white-space: pre-line;
|
||||
}
|
||||
.site-message-meta {
|
||||
margin-top: 7px;
|
||||
color: var(--text-tertiary);
|
||||
font-size: var(--font-size-xs);
|
||||
}
|
||||
.site-message-inline-cta,
|
||||
.site-message-card-cta {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 32px;
|
||||
color: var(--accent);
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 700;
|
||||
text-decoration: none;
|
||||
}
|
||||
.site-message-inline-cta {
|
||||
margin-top: 8px;
|
||||
}
|
||||
.site-message-inline-cta:hover,
|
||||
.site-message-card-cta:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.site-message-announcement-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
.site-message-announcement {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: 14px;
|
||||
padding: 13px 14px 13px 16px;
|
||||
border: 1px solid var(--border-primary);
|
||||
border-radius: 12px;
|
||||
background:
|
||||
linear-gradient(135deg, color-mix(in srgb, var(--accent) 5%, transparent), transparent 58%),
|
||||
var(--bg-card);
|
||||
overflow: hidden;
|
||||
box-shadow: 0 1px 0 rgba(15, 23, 42, 0.04);
|
||||
}
|
||||
.site-message-announcement::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0 auto 0 0;
|
||||
width: 3px;
|
||||
background: var(--accent);
|
||||
}
|
||||
.site-message-announcement.level-critical {
|
||||
border-color: color-mix(in srgb, var(--error) 30%, var(--border-primary));
|
||||
background: color-mix(in srgb, var(--error) 7%, var(--bg-card));
|
||||
}
|
||||
.site-message-announcement.level-critical::before {
|
||||
background: var(--error);
|
||||
}
|
||||
.site-message-announcement.level-warning {
|
||||
border-color: color-mix(in srgb, var(--warning) 34%, var(--border-primary));
|
||||
background: color-mix(in srgb, var(--warning) 8%, var(--bg-card));
|
||||
}
|
||||
.site-message-announcement.level-warning::before {
|
||||
background: var(--warning);
|
||||
}
|
||||
.site-message-announcement.level-success::before {
|
||||
background: var(--success);
|
||||
}
|
||||
.site-message-announcement-main {
|
||||
min-width: 0;
|
||||
}
|
||||
.site-message-announcement-kicker {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
margin-bottom: 8px;
|
||||
color: var(--accent);
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: 760;
|
||||
}
|
||||
.site-message-announcement-kicker small {
|
||||
padding: 3px 7px;
|
||||
border-radius: 999px;
|
||||
background: color-mix(in srgb, var(--bg-tertiary) 82%, transparent);
|
||||
color: var(--text-tertiary);
|
||||
font-size: 10px;
|
||||
line-height: 1;
|
||||
font-weight: 700;
|
||||
}
|
||||
.site-message-announcement h3 {
|
||||
margin: 0;
|
||||
color: var(--text-primary);
|
||||
font-size: 15px;
|
||||
font-weight: 740;
|
||||
line-height: 1.35;
|
||||
}
|
||||
.site-message-announcement p {
|
||||
max-width: 520px;
|
||||
margin: 7px 0 0;
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--font-size-sm);
|
||||
line-height: 1.6;
|
||||
white-space: pre-line;
|
||||
}
|
||||
.site-message-announcement-side {
|
||||
min-width: 118px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
color: var(--text-tertiary);
|
||||
font-size: var(--font-size-xs);
|
||||
text-align: right;
|
||||
}
|
||||
.site-message-card-cta {
|
||||
min-height: 32px;
|
||||
padding: 0 11px;
|
||||
border: 1px solid color-mix(in srgb, var(--accent) 30%, transparent);
|
||||
border-radius: 9px;
|
||||
background: color-mix(in srgb, var(--accent) 9%, transparent);
|
||||
text-decoration: none;
|
||||
}
|
||||
.site-message-card-cta:hover {
|
||||
background: color-mix(in srgb, var(--accent) 14%, transparent);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.site-message-overlay {
|
||||
align-items: flex-start;
|
||||
padding: 10px;
|
||||
}
|
||||
.site-message-modal {
|
||||
width: 100%;
|
||||
max-height: calc(100vh - 20px);
|
||||
border-radius: 14px;
|
||||
}
|
||||
.site-message-header {
|
||||
grid-template-columns: 1fr auto;
|
||||
gap: 10px;
|
||||
padding: 14px 14px 10px;
|
||||
}
|
||||
.site-message-header h2 {
|
||||
font-size: 18px;
|
||||
}
|
||||
.site-message-title-icon {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
.site-message-tabs {
|
||||
grid-column: 1 / -1;
|
||||
order: 3;
|
||||
width: 100%;
|
||||
}
|
||||
.site-message-tab {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
min-height: 38px;
|
||||
padding: 0 8px;
|
||||
}
|
||||
.site-message-tab span {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.site-message-body {
|
||||
padding: 14px;
|
||||
}
|
||||
.site-message-section-head {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
.site-message-timeline-content {
|
||||
padding: 12px;
|
||||
}
|
||||
.site-message-item-top {
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
.site-message-announcement {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 12px;
|
||||
padding: 14px 13px 14px 17px;
|
||||
}
|
||||
.site-message-announcement-side {
|
||||
min-width: 0;
|
||||
align-items: flex-start;
|
||||
text-align: left;
|
||||
}
|
||||
.site-message-footer {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
padding: 10px 14px 14px;
|
||||
}
|
||||
.site-message-footer .btn {
|
||||
width: 100%;
|
||||
min-height: 40px;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user