diff --git a/sidepanel/contribution-content-update-service.js b/sidepanel/contribution-content-update-service.js new file mode 100644 index 0000000..53db4e1 --- /dev/null +++ b/sidepanel/contribution-content-update-service.js @@ -0,0 +1,147 @@ +(() => { + const PORTAL_BASE_URL = 'https://apikey.qzz.io'; + const CONTENT_SUMMARY_API_URL = `${PORTAL_BASE_URL}/api/content-summary`; + const CACHE_KEY = 'multipage-contribution-content-summary-v1'; + const FETCH_TIMEOUT_MS = 6000; + + function sanitizeItem(item = {}) { + return { + slug: String(item?.slug || '').trim(), + title: String(item?.title || '').trim(), + isEnabled: Boolean(item?.is_enabled), + hasContent: Boolean(item?.has_content), + isVisible: Boolean(item?.is_visible), + updatedAt: String(item?.updated_at || '').trim(), + updatedAtDisplay: String(item?.updated_at_display || '').trim(), + }; + } + + function buildSnapshot(payload = {}) { + const items = Array.isArray(payload?.items) + ? payload.items.map(sanitizeItem).filter((item) => item.slug) + : []; + const promptVersion = String(payload?.prompt_version || '').trim(); + const latestUpdatedAt = String(payload?.latest_updated_at || '').trim(); + const latestUpdatedAtDisplay = String(payload?.latest_updated_at_display || '').trim(); + const hasVisibleUpdates = Boolean(payload?.has_visible_updates) && Boolean(promptVersion); + + return { + status: hasVisibleUpdates ? 'update-available' : 'idle', + promptVersion, + hasVisibleUpdates, + latestUpdatedAt, + latestUpdatedAtDisplay, + items, + portalUrl: PORTAL_BASE_URL, + apiUrl: CONTENT_SUMMARY_API_URL, + checkedAt: Date.now(), + }; + } + + function readCache() { + try { + const raw = localStorage.getItem(CACHE_KEY); + if (!raw) { + return null; + } + + const parsed = JSON.parse(raw); + if (!parsed || typeof parsed !== 'object') { + return null; + } + + const snapshot = buildSnapshot({ + items: parsed.items, + prompt_version: parsed.promptVersion, + has_visible_updates: parsed.hasVisibleUpdates, + latest_updated_at: parsed.latestUpdatedAt, + latest_updated_at_display: parsed.latestUpdatedAtDisplay, + }); + if (!Number.isFinite(parsed.checkedAt)) { + return snapshot; + } + snapshot.checkedAt = parsed.checkedAt; + return snapshot; + } catch (error) { + return null; + } + } + + function writeCache(snapshot) { + try { + localStorage.setItem(CACHE_KEY, JSON.stringify(snapshot)); + } catch (error) { + // Ignore cache write failures. + } + } + + async function fetchContentSummary() { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS); + + try { + const response = await fetch(CONTENT_SUMMARY_API_URL, { + method: 'GET', + headers: { + Accept: 'application/json', + }, + cache: 'no-store', + signal: controller.signal, + }); + + if (!response.ok) { + throw new Error(`内容摘要请求失败:${response.status}`); + } + + const payload = await response.json(); + if (!payload || payload.ok !== true) { + throw new Error('内容摘要返回格式异常'); + } + + const snapshot = buildSnapshot(payload); + writeCache(snapshot); + return snapshot; + } catch (error) { + if (error?.name === 'AbortError') { + throw new Error('内容摘要请求超时'); + } + throw error; + } finally { + clearTimeout(timeoutId); + } + } + + async function getContentUpdateSnapshot() { + try { + return await fetchContentSummary(); + } catch (error) { + const cached = readCache(); + if (cached) { + return { + ...cached, + fromCache: true, + errorMessage: error?.message || '内容摘要获取失败', + }; + } + + return { + status: 'error', + promptVersion: '', + hasVisibleUpdates: false, + latestUpdatedAt: '', + latestUpdatedAtDisplay: '', + items: [], + portalUrl: PORTAL_BASE_URL, + apiUrl: CONTENT_SUMMARY_API_URL, + checkedAt: Date.now(), + errorMessage: error?.message || '内容摘要获取失败', + }; + } + } + + window.SidepanelContributionContentService = { + getContentUpdateSnapshot, + portalUrl: PORTAL_BASE_URL, + apiUrl: CONTENT_SUMMARY_API_URL, + }; +})(); diff --git a/sidepanel/sidepanel.css b/sidepanel/sidepanel.css index 09e8a71..87558d9 100644 --- a/sidepanel/sidepanel.css +++ b/sidepanel/sidepanel.css @@ -243,6 +243,76 @@ header { color: var(--orange); } +.contribution-entry { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 6px; + position: relative; +} + +.contribution-update-hint { + position: relative; + display: flex; + align-items: flex-start; + gap: 8px; + max-width: 220px; + margin-left: 6px; + padding: 8px 10px; + background: color-mix(in srgb, var(--amber) 10%, var(--bg-base)); + border: 1px solid color-mix(in srgb, var(--amber) 34%, var(--border)); + border-radius: 10px; + box-shadow: var(--shadow-sm); +} + +.contribution-update-hint::before { + content: ''; + position: absolute; + top: -7px; + left: 14px; + width: 12px; + height: 12px; + background: inherit; + border-top: inherit; + border-left: inherit; + transform: rotate(45deg); +} + +.contribution-update-hint-text { + font-size: 12px; + line-height: 1.45; + color: var(--text-primary); +} + +.contribution-update-hint-close { + display: inline-flex; + align-items: center; + justify-content: center; + width: 20px; + height: 20px; + padding: 0; + border: none; + border-radius: 999px; + background: transparent; + color: var(--text-secondary); + font: inherit; + font-size: 14px; + line-height: 1; + cursor: pointer; + flex-shrink: 0; + transition: background var(--transition), color var(--transition); +} + +.contribution-update-hint-close:hover { + background: color-mix(in srgb, var(--amber) 16%, transparent); + color: var(--text-primary); +} + +.contribution-update-hint-close:focus-visible { + outline: 2px solid color-mix(in srgb, var(--amber) 34%, transparent); + outline-offset: 1px; +} + /* ============================================================ Theme Toggle ============================================================ */ @@ -366,7 +436,7 @@ header { .run-group { display: flex; - align-items: center; + align-items: flex-start; gap: 4px; flex-wrap: wrap; } diff --git a/sidepanel/sidepanel.html b/sidepanel/sidepanel.html index 1d75231..b4f4081 100644 --- a/sidepanel/sidepanel.html +++ b/sidepanel/sidepanel.html @@ -34,8 +34,17 @@
+ 公告 / 使用教程有更新了,可点上方“贡献/使用”查看。 +
+ +