feat: 分享传播与经文纸水墨背景

- 分享按钮:系统分享 + 二维码卡片(qr.png),移动端移入播放行不再遮挡标题
- og 分享卡片元数据 + og.png 封面;beforeinstallprompt 安装提示
- robots.txt / sitemap.xml / poster.png 传播物料
- .sutra-paper 宣纸底纹:桌面 bg.jpg,移动端竖版 bg-m.jpg,夜间模式罩层适配
- 移动端设置区重排为双列标签网格

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
selfrelease
2026-10-05 14:30:43 +08:00
co-authored by Devin
parent 490f032d60
commit 6329f3f6b4
13 changed files with 215 additions and 6 deletions
+61
View File
@@ -1037,3 +1037,64 @@ function openAdmin() {
loadStats();
card.querySelector("#admin-refresh").addEventListener("click", loadStats);
}
// ===== 分享与 PWA 安装提示 =====
const btnShare = document.getElementById("btn-share");
let installPromptEvt = null; // beforeinstallprompt 事件暂存,供分享卡片内"安装到主屏幕"使用
window.addEventListener("beforeinstallprompt", (e) => {
e.preventDefault();
installPromptEvt = e;
});
/** 分享本站:两端统一弹二维码卡片,手机端贴底升起;支持系统分享时另有"发送给朋友"。 */
btnShare.addEventListener("click", () => {
openShareCard(location.origin + "/");
});
/** 分享卡片:二维码扫码即达 + 复制链接 + 系统分享(如可用)+ 可安装时显示安装按钮。 */
function openShareCard(url) {
const mask = document.createElement("div");
mask.className = "admin-mask share";
const card = document.createElement("div");
card.className = "admin-card share-card";
card.setAttribute("role", "dialog");
card.setAttribute("aria-label", "分享本站");
card.innerHTML = `
<div class="admin-title">分享本站</div>
<img class="share-qr" src="qr.png" alt="本站二维码">
<span class="share-link">${url}</span>
<div class="admin-actions">
${navigator.share ? '<button type="button" class="admin-btn" id="share-native">发送给朋友</button>' : ""}
<button type="button" class="admin-btn" id="share-copy">复制链接</button>
${installPromptEvt ? '<button type="button" class="admin-btn" id="share-install">安装到主屏幕</button>' : ""}
<button type="button" class="admin-btn" id="share-close">关闭</button>
</div>`;
mask.appendChild(card);
document.body.appendChild(mask);
const close = () => mask.remove();
mask.addEventListener("click", (e) => { if (e.target === mask) close(); });
card.querySelector("#share-close").addEventListener("click", close);
card.querySelector("#share-native")?.addEventListener("click", async () => {
try {
await navigator.share({
title: "经典诵读",
text: "心经、金刚经、道德经等经典在线诵读:千问语音合成,逐字高亮跟读,山寺配乐。",
url,
});
} catch { /* 用户取消分享 */ }
});
card.querySelector("#share-copy").addEventListener("click", async (e) => {
try {
await navigator.clipboard.writeText(url);
e.target.textContent = "已复制";
} catch {
e.target.textContent = "复制失败";
}
});
card.querySelector("#share-install")?.addEventListener("click", async () => {
installPromptEvt.prompt();
await installPromptEvt.userChoice;
installPromptEvt = null;
close();
});
}
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

+1 -1
View File
@@ -14,7 +14,7 @@ HOST=154.8.162.18
USER=root
DEST=/opt/fj
# 注意:不同步 .env —— 服务器端 PORT=8018 与本地开发配置不同,避免覆盖。
FILES=(index.html style.css app.js server.py sw.js manifest.json icon.svg icon-180.png icon-192.png icon-512.png README.md run.md)
FILES=(index.html style.css app.js server.py sw.js manifest.json icon.svg icon-180.png icon-192.png icon-512.png README.md run.md robots.txt sitemap.xml og.png qr.png poster.png bg.jpg bg-m.jpg)
SSH_OPTS="-o StrictHostKeyChecking=no -o ConnectTimeout=15"
SSH_CMD="ssh $SSH_OPTS"
+10 -3
View File
@@ -5,7 +5,13 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="theme-color" content="#f6f1e6">
<meta name="description" content="心经、金刚经、阿弥陀经、普门品、道德经、清静经、太上感应篇诵读——千问语音合成,晨钟暮鼓配乐,逐段跟读高亮。">
<title>心经</title>
<title>经典诵读 · 心经</title>
<meta property="og:type" content="website">
<meta property="og:title" content="经典诵读 · 佛经道家经典在线诵读">
<meta property="og:description" content="心经、金刚经、道德经等七部经典:千问语音诵读,逐字高亮跟读,山寺晨钟配乐。">
<meta property="og:url" content="https://fj.all8ai.top/">
<meta property="og:image" content="https://fj.all8ai.top/og.png">
<meta name="twitter:card" content="summary_large_image">
<link rel="icon" type="image/svg+xml" href="icon.svg">
<link rel="apple-touch-icon" href="icon-180.png">
<link rel="manifest" href="manifest.json">
@@ -13,7 +19,7 @@
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="default">
<meta name="apple-mobile-web-app-title" content="经典诵读">
<link rel="stylesheet" href="style.css?v=11">
<link rel="stylesheet" href="style.css?v=17">
</head>
<body>
@@ -90,6 +96,7 @@
</div>
</div>
<div id="status" class="status" role="status"></div>
<button type="button" id="btn-share" class="share-btn" aria-label="分享本站">分享</button>
<button type="button" id="btn-expand" class="expand-btn" aria-expanded="false" aria-label="展开设置" title="设置">
<span class="expand-label">设置</span>
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09a1.65 1.65 0 0 0-1-1.51 1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09a1.65 1.65 0 0 0 1.51-1 1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>
@@ -116,6 +123,6 @@
</footer>
<audio id="audio" preload="auto"></audio>
<script src="app.js?v=13"></script>
<script src="app.js?v=15"></script>
</body>
</html>
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 879 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 565 B

+5
View File
@@ -0,0 +1,5 @@
User-agent: *
Allow: /
Disallow: /api/
Sitemap: https://fj.all8ai.top/sitemap.xml
+2
View File
@@ -9,6 +9,8 @@
| TTS | 千问 `qwen3-tts-flash`,走 DashScope 兼容接口 `/api/v1/services/aigc/multimodal-generation/generation` |
| 经文数据 | `sutras/*.json` 外置数据文件,目录实时扫描,加经不改代码 |
| 背景音乐 | `bgm/` 目录音频文件循环播放(前端淡入淡出),deploy.sh 随 `sutras/` 一并同步 |
| 传播资源 | `og.png` 分享卡片图、`qr.png` 二维码、`poster.png` 海报、`bg.jpg` 经文底纹、`robots.txt`/`sitemap.xml` |
| 后台面板 | 连点页脚提示 6 次唤出,`GET /api/stats` 返回运行统计(访客 IP 取 X-Forwarded-For 末位) |
## 首次准备
+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://fj.all8ai.top/</loc>
<changefreq>monthly</changefreq>
<priority>1.0</priority>
</url>
</urlset>
+127 -1
View File
@@ -558,7 +558,18 @@ body.night .jump-now { background: rgba(43, 36, 27, 0.92); }
.size-btn { width: 34px; height: 30px; font-size: 0.8rem; }
.layout-btn, .sutra-btn, .pager-btn { padding: 5px 8px; font-size: 0.78rem; min-height: 30px; }
.sutra-paper { padding: 18px 16px; }
.sutra-paper {
padding: 18px 16px;
/* 手机端竖版水墨底纹:底部山寺剪影,上部留白供文字 */
background:
linear-gradient(rgba(253, 250, 242, 0.85), rgba(253, 250, 242, 0.85)),
url("bg-m.jpg") center bottom / cover no-repeat;
}
body.night .page .sutra-paper {
background:
linear-gradient(rgba(41, 34, 25, 0.88), rgba(41, 34, 25, 0.88)),
url("bg-m.jpg") center bottom / cover no-repeat;
}
.sutra-paper::before { inset: 6px; }
.sutra-paper::after { inset: 9px; }
.sutra { column-width: auto; }
@@ -775,3 +786,118 @@ body.night .seg.playing { background: rgba(201, 106, 92, 0.16); color: #f0c9c2;
}
.admin-btn:hover { border-color: var(--gold); color: var(--ink); }
body.night .admin-card { background: #241d15; }
/* ===== 分享按钮(固定右上,与左上 logo 呼应) ===== */
.share-btn {
position: fixed;
top: 18px;
right: 26px;
z-index: 30;
padding: 6px 14px;
font-family: inherit;
font-size: 0.78rem;
letter-spacing: 0.2em;
color: var(--cinnabar);
background: var(--paper);
border: 1px solid var(--gold);
border-radius: 6px;
cursor: pointer;
box-shadow: 0 3px 10px rgba(30, 22, 14, 0.12);
transition: color 0.15s ease, border-color 0.15s ease;
}
.share-btn:hover { color: var(--ink); }
.share-btn:focus-visible { outline: 3px solid var(--gold-soft); outline-offset: 1px; }
body.night .share-btn { background: #241d15; }
/* ===== 分享弹层(二维码 + 复制链接 + 安装) ===== */
.share-card .share-qr { display: block; width: 200px; margin: 8px auto; border: 1px solid var(--hairline); border-radius: 8px; }
.share-card .share-link {
display: block;
text-align: center;
font-size: 0.78rem;
color: var(--ink-soft);
word-break: break-all;
margin-bottom: 4px;
}
.share-card .admin-actions { flex-wrap: wrap; }
.share-card .admin-btn { flex: 1 1 45%; }
@media (max-width: 720px) {
/* 手机端分享按钮不再固定悬浮,进入播放行(收起态夹在"设置"前) */
.share-btn {
position: static;
order: 3;
flex: none;
height: 34px;
padding: 0 11px;
font-size: 0.72rem;
letter-spacing: 0.1em;
color: var(--ink-soft);
background: transparent;
border-color: var(--hairline);
box-shadow: none;
}
/* 展开态:与循环/夜间同行三等分 */
.player-card.expanded .share-btn { order: 9; flex: 1 1 28%; height: 30px; padding: 5px 8px; font-size: 0.78rem; }
.player-card.expanded .tool-group[aria-label="模式"] .layout-btn { flex: 1 1 28%; }
/* 分享卡片贴底升起 */
.admin-mask.share { align-items: flex-end; padding: 0; }
.admin-mask.share .share-card {
width: 100%;
border-radius: 12px 12px 0 0;
border-left: none;
border-right: none;
border-bottom: none;
max-height: 70vh;
padding-bottom: calc(16px + env(safe-area-inset-bottom));
}
}
/* ===== 桌面端经文纸底纹:山寺水墨淡化垫底,仅宽屏启用 ===== */
@media (min-width: 721px) {
/* 水墨底纹衬在经文纸(.sutra-paper)区域,不铺出纸外 */
.sutra-paper {
background:
linear-gradient(rgba(253, 250, 242, 0.84), rgba(253, 250, 242, 0.84)),
url("bg.jpg") center / cover no-repeat;
}
body.night .sutra-paper {
background:
linear-gradient(rgba(41, 34, 25, 0.87), rgba(41, 34, 25, 0.87)),
url("bg.jpg") center / cover no-repeat;
}
}
/* ===== 手机端展开态设置区重排:统一"小标签 + 控件"双列网格 ===== */
@media (max-width: 720px) {
.player-card.expanded .btn-play { order: 1; flex: 1 1 100%; }
.player-card.expanded .sleep-remaining.show { order: 2; flex: 1 1 100%; justify-content: center; }
/* 第一行:经典 | 音色 */
.player-card.expanded .player-row > .field-pair { order: 3; flex: 1 1 46%; }
.player-card.expanded .player-meta { order: 4; flex: 1 1 46%; gap: 8px; }
/* 第二行:字号 | 语速 */
.player-card.expanded .tool-group[aria-label="字号"] { order: 5; flex: 1 1 46%; justify-content: flex-start; gap: 8px; }
.player-card.expanded .size-btn { flex: 1; }
.player-card.expanded .tool-group[aria-label="模式"] { display: contents; }
.player-card.expanded .tool-group[aria-label="模式"] .field-pair { flex: 1 1 46%; }
.player-card.expanded .tool-group[aria-label="模式"] .field-pair:has(#rate-select) { order: 6; }
/* 第三行:配乐 | 定时 */
.player-card.expanded .tool-group[aria-label="模式"] .field-pair:has(#bgm-select) { order: 7; }
.player-card.expanded .tool-group[aria-label="模式"] .field-pair:has(#timer-select) { order: 8; }
/* 第四行:循环 | 夜间 */
.player-card.expanded .tool-group[aria-label="模式"] .layout-btn { flex: 1 1 46%; order: 9; }
.player-card.expanded .flex-break { display: none; }
.player-card.expanded .expand-btn { order: 11; flex: 1 1 100%; }
/* 每个单元格统一:金色小标签 + 控件 */
.player-card.expanded .tool-name,
.player-card.expanded .voice-label {
display: flex;
align-items: center;
font-size: 0.68rem;
color: var(--gold);
letter-spacing: 0.06em;
white-space: nowrap;
}
.player-card.expanded .field-pair .sheet-btn,
.player-card.expanded .player-meta .sheet-btn { flex: 1; min-width: 0; }
}
+1 -1
View File
@@ -4,7 +4,7 @@
* - /api/tts 不拦截,直接透传网络
*/
const CACHE = "fj-shell-v1";
const SHELL = ["/", "/index.html", "/style.css?v=11", "/app.js?v=13", "/icon.svg", "/icon-180.png", "/icon-192.png", "/icon-512.png", "/manifest.json"];
const SHELL = ["/", "/index.html", "/style.css?v=17", "/app.js?v=15", "/icon.svg", "/icon-180.png", "/icon-192.png", "/icon-512.png", "/manifest.json"];
self.addEventListener("install", (e) => {
e.waitUntil(caches.open(CACHE).then((c) => c.addAll(SHELL)).then(() => self.skipWaiting()));