feat(pwa): 瞬间页安装引导与启动快捷方式(车道 H)
- manifest shortcuts:瞬间直达(/moments/ + 复用 192 图标;lang
zh 单份中文)
- 安装 CTA:时间线后灰阶虚线小条+ghost 按钮;beforeinstallprompt
模块级立即绑定(跨软导航存活),三态=可装显示/已装
(display-mode:standalone 或 iOS standalone)永不显/不支持默认
hidden;prompt 后 accepted/appinstalled 清引用;文案组件内
locale 双份
- SW 复核零改动:PRECACHE 已含双轨瞬间、IMMUTABLE 覆盖
/_astro/ 瞬间图片管线、NEVER_CACHE 拦 pv 走 '–' 降级——三处
现成正确;离线实证:断网瞬间页 200+时间线完整+pv 降级;图片
0/0 为数据现状(3 条瞬间均无图),同管线文章页图片离线 1/1
等价实证成立
车道:op-artalk(自验 11/11);收编验收:CTA hidden 态+离线 200+
标题+manifest shortcuts 解析实测(首轮探针 context API 误用致假
FAIL,selector 复核全过)
验证: pnpm astro build exit 0;E2E 33/33;断网瞬间页 playwright
实测 200
Spec: none (PWA 增强:shortcuts+CTA+离线实证,车道级交付)
Co-authored-by: GLM-5.3
This commit is contained in:
1 parent
215760c38a
commit
73c31ffb2a
2 files changed
+78
No files matched your search
@@ -12,5 +12,14 @@
|
|||||||
{ "src": "/icons/icon-192.png", "sizes": "192x192", "type": "image/png" },
|
{ "src": "/icons/icon-192.png", "sizes": "192x192", "type": "image/png" },
|
||||||
{ "src": "/icons/icon-512.png", "sizes": "512x512", "type": "image/png" },
|
{ "src": "/icons/icon-512.png", "sizes": "512x512", "type": "image/png" },
|
||||||
{ "src": "/icons/icon-maskable-512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" }
|
{ "src": "/icons/icon-maskable-512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" }
|
||||||
|
],
|
||||||
|
"shortcuts": [
|
||||||
|
{
|
||||||
|
"name": "瞬间",
|
||||||
|
"short_name": "瞬间",
|
||||||
|
"description": "离线也能刷的瞬间时间线",
|
||||||
|
"url": "/moments/",
|
||||||
|
"icons": [{ "src": "/icons/icon-192.png", "sizes": "192x192" }]
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -56,6 +56,14 @@ const iso = (d: Date) => d.toISOString();
|
|||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ol>
|
</ol>
|
||||||
|
|
||||||
|
<!-- 安装引导(disclosure 小条):仅浏览器明确可装且未安装时现形,克制不打扰 -->
|
||||||
|
<div class="install-cta" id="install-cta" hidden>
|
||||||
|
<span>{locale === 'en' ? 'Add to Home Screen — moments work offline' : '装到主屏,离线也能刷瞬间'}</span>
|
||||||
|
<button type="button" id="install-btn" class="install-btn">
|
||||||
|
{locale === 'en' ? 'Install' : '安装'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</BaseLayout>
|
</BaseLayout>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@@ -103,9 +111,46 @@ const iso = (d: Date) => d.toISOString();
|
|||||||
document.addEventListener('astro:page-load', () => {
|
document.addEventListener('astro:page-load', () => {
|
||||||
void bumpPv();
|
void bumpPv();
|
||||||
armAnim();
|
armAnim();
|
||||||
|
refreshInstallCta();
|
||||||
});
|
});
|
||||||
void bumpPv();
|
void bumpPv();
|
||||||
armAnim();
|
armAnim();
|
||||||
|
|
||||||
|
// —— PWA 安装引导:beforeinstallprompt 早在页面加载初期触发,
|
||||||
|
// 故模块级立即绑定存事件,page-load 只负责刷新 UI 显隐 ——
|
||||||
|
let deferredPrompt: Event & { prompt?: () => Promise<void>; userChoice?: Promise<{ outcome: string }> } | null = null;
|
||||||
|
document.addEventListener('beforeinstallprompt', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
deferredPrompt = e as typeof deferredPrompt;
|
||||||
|
refreshInstallCta();
|
||||||
|
});
|
||||||
|
document.addEventListener('appinstalled', () => {
|
||||||
|
deferredPrompt = null;
|
||||||
|
refreshInstallCta();
|
||||||
|
});
|
||||||
|
const isInstalled = () =>
|
||||||
|
matchMedia('(display-mode: standalone)').matches ||
|
||||||
|
(navigator as Navigator & { standalone?: boolean }).standalone === true;
|
||||||
|
function refreshInstallCta() {
|
||||||
|
const cta = document.getElementById('install-cta');
|
||||||
|
if (!cta) return;
|
||||||
|
cta.hidden = !(deferredPrompt && !isInstalled());
|
||||||
|
}
|
||||||
|
document.addEventListener('click', (e) => {
|
||||||
|
if ((e.target as HTMLElement)?.id !== 'install-btn') return;
|
||||||
|
if (!deferredPrompt) return;
|
||||||
|
void (async () => {
|
||||||
|
try {
|
||||||
|
await deferredPrompt?.prompt?.();
|
||||||
|
const choice = await deferredPrompt?.userChoice;
|
||||||
|
if (choice?.outcome === 'accepted') {
|
||||||
|
deferredPrompt = null;
|
||||||
|
refreshInstallCta();
|
||||||
|
}
|
||||||
|
} catch { /* 用户关闭安装框:保留事件,小条仍在 */ }
|
||||||
|
})();
|
||||||
|
});
|
||||||
|
refreshInstallCta();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@@ -201,6 +246,30 @@ const iso = (d: Date) => d.toISOString();
|
|||||||
.mom-card { padding: var(--space-2) var(--space-3); }
|
.mom-card { padding: var(--space-2) var(--space-3); }
|
||||||
.mom-fig { max-width: 100%; }
|
.mom-fig { max-width: 100%; }
|
||||||
}
|
}
|
||||||
|
.install-cta {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-2);
|
||||||
|
margin-top: var(--space-5);
|
||||||
|
padding: var(--space-3);
|
||||||
|
border: 1px dashed var(--line);
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--ink-3);
|
||||||
|
}
|
||||||
|
.install-btn {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--ink-2);
|
||||||
|
background: var(--bg-soft);
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 4px 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border-color var(--dur) var(--ease);
|
||||||
|
}
|
||||||
|
.install-btn:hover { border-color: var(--accent); }
|
||||||
.pv-line {
|
.pv-line {
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
|||||||
Reference in new issue
Block a user