feat: 循环播放、单音频拼接播放、程序合成晨钟暮鼓配乐
- 播放改为全文拼接成单个 WAV:段间 0.8s/尾部 3s 静音写入文件, audio.loop 原生循环,解决手机锁屏/后台时定时器冻结导致的中断 - 新增"循环"开关与"配乐"开关 - 背景音乐 Web Audio 实时合成:低频 drone 垫底(相邻根音慢滑), 晨钟(大钟非谐泛音簇)+ 暮鼓(膜鸣+击边噪声)+ 引磬稀疏点缀 - deploy.sh 一键部署(rsync + 重启 + 健康检查,不同步 .env) - 新增页签图标与桌面 logo Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
@@ -103,6 +103,7 @@ const btnExpand = document.getElementById("btn-expand");
|
|||||||
const btnPrev = document.getElementById("btn-prev");
|
const btnPrev = document.getElementById("btn-prev");
|
||||||
const btnNext = document.getElementById("btn-next");
|
const btnNext = document.getElementById("btn-next");
|
||||||
const btnLoop = document.getElementById("btn-loop");
|
const btnLoop = document.getElementById("btn-loop");
|
||||||
|
const btnBgm = document.getElementById("btn-bgm");
|
||||||
const pageInfo = document.getElementById("page-info");
|
const pageInfo = document.getElementById("page-info");
|
||||||
|
|
||||||
/** 音频缓存:key = `${经id}|${音色}|${段序号}`,value = Blob */
|
/** 音频缓存:key = `${经id}|${音色}|${段序号}`,value = Blob */
|
||||||
@@ -111,9 +112,279 @@ let playToken = 0; // 播放会话令牌,防止旧异步任务串扰
|
|||||||
let playing = false;
|
let playing = false;
|
||||||
let inflight = null; // 进行中的 fetch AbortController
|
let inflight = null; // 进行中的 fetch AbortController
|
||||||
let loopMode = false; // 循环诵读开关:播完全文回到开头继续
|
let loopMode = false; // 循环诵读开关:播完全文回到开头继续
|
||||||
|
let bgmOn = false; // 背景音乐开关
|
||||||
let segTimes = []; // 合并音频中各段的起始秒
|
let segTimes = []; // 合并音频中各段的起始秒
|
||||||
let currentSeg = -1; // 正在朗读的段号
|
let currentSeg = -1; // 正在朗读的段号
|
||||||
|
|
||||||
|
// ===== 背景音乐:Web Audio 实时合成颂钵泛音 drone(零资源文件、无缝循环) =====
|
||||||
|
let bgmCtx = null;
|
||||||
|
let bgmGain = null;
|
||||||
|
let bgmNodes = [];
|
||||||
|
let bgmBellTimer = null;
|
||||||
|
let bgmDrumTimer = null;
|
||||||
|
let bgmChimeTimer = null;
|
||||||
|
let bgmGlideTimer = null;
|
||||||
|
let bgmOscs = []; // {osc, ratio},供基频滑变统一改频率
|
||||||
|
const BGM_ROOTS = [55, 65.41, 73.42, 82.41, 98]; // A1 C2 D2 E2 G2,低音区垫底
|
||||||
|
let bgmRoot = 73.42;
|
||||||
|
|
||||||
|
/** 基频慢移:只滑向相邻根音、16 秒缓慢过渡,变化若有似无,避免警报式的滑音。 */
|
||||||
|
function bgmGlide() {
|
||||||
|
const idx = BGM_ROOTS.indexOf(bgmRoot);
|
||||||
|
const neighbors = [idx - 1, idx + 1].filter((i) => i >= 0 && i < BGM_ROOTS.length);
|
||||||
|
bgmRoot = BGM_ROOTS[neighbors[Math.floor(Math.random() * neighbors.length)]];
|
||||||
|
const t = bgmCtx.currentTime;
|
||||||
|
for (const { osc, ratio } of bgmOscs) {
|
||||||
|
osc.frequency.cancelScheduledValues(t);
|
||||||
|
osc.frequency.setValueAtTime(osc.frequency.value, t);
|
||||||
|
osc.frequency.exponentialRampToValueAtTime(bgmRoot * ratio, t + 16);
|
||||||
|
}
|
||||||
|
bgmGlideTimer = setTimeout(() => { if (bgmGain) bgmGlide(); }, 50000 + Math.random() * 40000);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 给打击声部接随机方位的输出链,返回挂到主增益下的 GainNode。 */
|
||||||
|
function bgmStrikeOut(panRange) {
|
||||||
|
const out = bgmCtx.createGain();
|
||||||
|
if (bgmCtx.createStereoPanner) {
|
||||||
|
const pan = bgmCtx.createStereoPanner();
|
||||||
|
pan.pan.value = (Math.random() - 0.5) * 2 * panRange;
|
||||||
|
out.connect(pan);
|
||||||
|
pan.connect(bgmGain);
|
||||||
|
} else {
|
||||||
|
out.connect(bgmGain);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 晨钟一声:大钟的非谐泛音簇,毫秒级起音,泛音越高衰减越快(长达十余秒)。 */
|
||||||
|
function bgmBell() {
|
||||||
|
const now = bgmCtx.currentTime;
|
||||||
|
const root = [98, 110, 123.47][Math.floor(Math.random() * 3)]; // G2 / A2 / B2 附近的大钟
|
||||||
|
const bellGain = bgmStrikeOut(0.8);
|
||||||
|
bellGain.gain.setValueAtTime(0, now);
|
||||||
|
bellGain.gain.linearRampToValueAtTime(0.5, now + 0.02);
|
||||||
|
// 大钟泛音列:哼音 0.5、基音 1、小三度 1.19、五度 1.5、名义音 2、上泛音 2.74
|
||||||
|
// [频率比, 振幅, 衰减秒数] —— 低频泛音余韵更长
|
||||||
|
[[0.5, 0.5, 15], [1, 1, 13], [1.19, 0.3, 9], [1.5, 0.24, 7], [2.0, 0.18, 5], [2.74, 0.1, 3.5]].forEach(([ratio, amp, dur]) => {
|
||||||
|
const o = bgmCtx.createOscillator();
|
||||||
|
o.type = "sine";
|
||||||
|
o.frequency.value = root * ratio;
|
||||||
|
o.detune.value = (Math.random() - 0.5) * 5;
|
||||||
|
const og = bgmCtx.createGain();
|
||||||
|
og.gain.setValueAtTime(amp, now);
|
||||||
|
og.gain.exponentialRampToValueAtTime(0.0001, now + dur);
|
||||||
|
o.connect(og);
|
||||||
|
og.connect(bellGain);
|
||||||
|
o.start(now);
|
||||||
|
o.stop(now + dur + 0.5);
|
||||||
|
bgmNodes.push(o);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 磬声一声:高音引磬,快起音 + 约 6 秒衰减的小泛音簇,稀疏点缀。 */
|
||||||
|
function bgmChime() {
|
||||||
|
const now = bgmCtx.currentTime;
|
||||||
|
const freq = [392, 523.25, 659.25, 783.99][Math.floor(Math.random() * 4)]; // 五声音阶附近
|
||||||
|
const chimeGain = bgmStrikeOut(0.6);
|
||||||
|
chimeGain.gain.setValueAtTime(0, now);
|
||||||
|
chimeGain.gain.linearRampToValueAtTime(0.35, now + 0.03);
|
||||||
|
chimeGain.gain.exponentialRampToValueAtTime(0.0001, now + 6);
|
||||||
|
[[1, 1], [2.76, 0.28], [5.4, 0.08]].forEach(([ratio, amp]) => {
|
||||||
|
const o = bgmCtx.createOscillator();
|
||||||
|
o.type = "sine";
|
||||||
|
o.frequency.value = freq * ratio;
|
||||||
|
o.detune.value = (Math.random() - 0.5) * 6;
|
||||||
|
const og = bgmCtx.createGain();
|
||||||
|
og.gain.value = amp;
|
||||||
|
o.connect(og);
|
||||||
|
og.connect(chimeGain);
|
||||||
|
o.start(now);
|
||||||
|
o.stop(now + 6.5);
|
||||||
|
bgmNodes.push(o);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 暮鼓一记:低频膜鸣(72→44Hz 下滑)+ 短促噪声击边,钝而沉。 */
|
||||||
|
function bgmDrum() {
|
||||||
|
const now = bgmCtx.currentTime;
|
||||||
|
const drumGain = bgmStrikeOut(0.5);
|
||||||
|
drumGain.gain.setValueAtTime(0, now);
|
||||||
|
drumGain.gain.linearRampToValueAtTime(0.55, now + 0.012);
|
||||||
|
drumGain.gain.exponentialRampToValueAtTime(0.0001, now + 1.8);
|
||||||
|
|
||||||
|
// 鼓皮基音:起振约 72Hz,迅速下滑到 44Hz
|
||||||
|
const o = bgmCtx.createOscillator();
|
||||||
|
o.type = "sine";
|
||||||
|
o.frequency.setValueAtTime(72, now);
|
||||||
|
o.frequency.exponentialRampToValueAtTime(44, now + 0.3);
|
||||||
|
o.connect(drumGain);
|
||||||
|
o.start(now);
|
||||||
|
o.stop(now + 2);
|
||||||
|
bgmNodes.push(o);
|
||||||
|
|
||||||
|
// 击边噪声:0.08s 噪声过 260Hz 带通,模拟鼓槌触皮的一声"扑"
|
||||||
|
const len = Math.floor(bgmCtx.sampleRate * 0.08);
|
||||||
|
const buf = bgmCtx.createBuffer(1, len, bgmCtx.sampleRate);
|
||||||
|
const data = buf.getChannelData(0);
|
||||||
|
for (let i = 0; i < len; i += 1) data[i] = Math.random() * 2 - 1;
|
||||||
|
const noise = bgmCtx.createBufferSource();
|
||||||
|
noise.buffer = buf;
|
||||||
|
const bp = bgmCtx.createBiquadFilter();
|
||||||
|
bp.type = "bandpass";
|
||||||
|
bp.frequency.value = 260;
|
||||||
|
bp.Q.value = 0.8;
|
||||||
|
const ng = bgmCtx.createGain();
|
||||||
|
ng.gain.setValueAtTime(0.4, now);
|
||||||
|
ng.gain.exponentialRampToValueAtTime(0.0001, now + 0.15);
|
||||||
|
noise.connect(bp);
|
||||||
|
bp.connect(ng);
|
||||||
|
ng.connect(drumGain);
|
||||||
|
noise.start(now);
|
||||||
|
bgmNodes.push(noise);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开始播放背景音乐:低频颂钵式泛音 drone 垫底(基频缓慢游走),
|
||||||
|
* 叠稀疏的晨钟(大钟非谐泛音簇、长余韵)、暮鼓(低频膜鸣 + 击边噪声)
|
||||||
|
* 与引磬(高音泛音点缀),三种寺院打击声部随机方位错开出现,
|
||||||
|
* 方位随机、音量压在人声之下;2.5 秒淡入。需在用户手势内调用。
|
||||||
|
*/
|
||||||
|
function startBgm() {
|
||||||
|
if (!bgmCtx) bgmCtx = new (window.AudioContext || window.webkitAudioContext)();
|
||||||
|
if (bgmCtx.state === "suspended") bgmCtx.resume();
|
||||||
|
const now = bgmCtx.currentTime;
|
||||||
|
const g = bgmCtx.createGain();
|
||||||
|
g.gain.setValueAtTime(0, now);
|
||||||
|
// 低通只做轻微软化,保留高频空气感(太低会发闷)
|
||||||
|
const lp = bgmCtx.createBiquadFilter();
|
||||||
|
lp.type = "lowpass";
|
||||||
|
lp.frequency.value = 3200;
|
||||||
|
lp.Q.value = 0.4;
|
||||||
|
g.connect(lp);
|
||||||
|
lp.connect(bgmCtx.destination);
|
||||||
|
|
||||||
|
const base = bgmRoot; // 初始根音,之后由 bgmGlide 慢移
|
||||||
|
const partials = [[1, 0.45], [2.02, 0.2], [3.01, 0.13], [4.16, 0.08], [5.43, 0.05], [7.2, 0.025]];
|
||||||
|
const nodes = [];
|
||||||
|
bgmOscs = [];
|
||||||
|
partials.forEach(([ratio, amp], pi) => {
|
||||||
|
const og = bgmCtx.createGain();
|
||||||
|
og.gain.value = amp;
|
||||||
|
// 泛音左右宽展开 + 极慢声像漂移,空间缓慢呼吸
|
||||||
|
if (bgmCtx.createStereoPanner) {
|
||||||
|
const pan = bgmCtx.createStereoPanner();
|
||||||
|
const side = pi % 2 === 0 ? -1 : 1;
|
||||||
|
pan.pan.value = side * 0.6;
|
||||||
|
og.connect(pan);
|
||||||
|
pan.connect(g);
|
||||||
|
const plfo = bgmCtx.createOscillator();
|
||||||
|
plfo.frequency.value = 0.02 + Math.random() * 0.03;
|
||||||
|
const plg = bgmCtx.createGain();
|
||||||
|
plg.gain.value = 0.3;
|
||||||
|
plfo.connect(plg);
|
||||||
|
plg.connect(pan.pan);
|
||||||
|
plfo.start(now + pi * 0.5);
|
||||||
|
nodes.push(plfo);
|
||||||
|
} else {
|
||||||
|
og.connect(g);
|
||||||
|
}
|
||||||
|
// 双振荡器 ±3 音分失谐,产生颂钵的拍频颤动
|
||||||
|
for (const det of [-3, 3]) {
|
||||||
|
const osc = bgmCtx.createOscillator();
|
||||||
|
osc.type = "sine";
|
||||||
|
osc.frequency.value = base * ratio;
|
||||||
|
osc.detune.value = det + (Math.random() - 0.5) * 2;
|
||||||
|
osc.connect(og);
|
||||||
|
osc.start(now + Math.random() * 0.3);
|
||||||
|
nodes.push(osc);
|
||||||
|
bgmOscs.push({ osc, ratio });
|
||||||
|
}
|
||||||
|
// 每个泛音带独立慢速呼吸,避免死板
|
||||||
|
const lfo = bgmCtx.createOscillator();
|
||||||
|
lfo.frequency.value = 0.04 + Math.random() * 0.07;
|
||||||
|
const lg = bgmCtx.createGain();
|
||||||
|
lg.gain.value = amp * 0.4;
|
||||||
|
lfo.connect(lg);
|
||||||
|
lg.connect(og.gain);
|
||||||
|
lfo.start(now);
|
||||||
|
nodes.push(lfo);
|
||||||
|
});
|
||||||
|
g.gain.linearRampToValueAtTime(0.07, now + 2.5); // 淡入,drone 压低垫底
|
||||||
|
bgmGain = g;
|
||||||
|
bgmNodes = nodes;
|
||||||
|
|
||||||
|
// 晨钟:淡入后 4 秒先撞一声,之后每 30~55 秒一声
|
||||||
|
const scheduleBell = () => {
|
||||||
|
bgmBellTimer = setTimeout(() => {
|
||||||
|
if (!bgmGain) return;
|
||||||
|
bgmBell();
|
||||||
|
scheduleBell();
|
||||||
|
}, 30000 + Math.random() * 25000);
|
||||||
|
};
|
||||||
|
bgmBellTimer = setTimeout(() => {
|
||||||
|
if (!bgmGain) return;
|
||||||
|
bgmBell();
|
||||||
|
scheduleBell();
|
||||||
|
}, 4000);
|
||||||
|
|
||||||
|
// 暮鼓:每 45~80 秒一记,偶尔连击两记(间隔约 2.4s)
|
||||||
|
const scheduleDrum = () => {
|
||||||
|
bgmDrumTimer = setTimeout(() => {
|
||||||
|
if (!bgmGain) return;
|
||||||
|
bgmDrum();
|
||||||
|
if (Math.random() < 0.45) setTimeout(() => { if (bgmGain) bgmDrum(); }, 2400);
|
||||||
|
scheduleDrum();
|
||||||
|
}, 45000 + Math.random() * 35000);
|
||||||
|
};
|
||||||
|
scheduleDrum();
|
||||||
|
|
||||||
|
// 引磬:最稀疏,每 60~100 秒一声清越点缀
|
||||||
|
const scheduleChime = () => {
|
||||||
|
bgmChimeTimer = setTimeout(() => {
|
||||||
|
if (!bgmGain) return;
|
||||||
|
bgmChime();
|
||||||
|
scheduleChime();
|
||||||
|
}, 60000 + Math.random() * 40000);
|
||||||
|
};
|
||||||
|
scheduleChime();
|
||||||
|
|
||||||
|
// 首次基频滑变:约 50 秒后
|
||||||
|
bgmGlideTimer = setTimeout(() => { if (bgmGain) bgmGlide(); }, 50000);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 停止背景音乐:1.5 秒淡出后停掉振荡器、钟鼓调度器并断开节点。 */
|
||||||
|
function stopBgm() {
|
||||||
|
if (bgmBellTimer) {
|
||||||
|
clearTimeout(bgmBellTimer);
|
||||||
|
bgmBellTimer = null;
|
||||||
|
}
|
||||||
|
if (bgmDrumTimer) {
|
||||||
|
clearTimeout(bgmDrumTimer);
|
||||||
|
bgmDrumTimer = null;
|
||||||
|
}
|
||||||
|
if (bgmChimeTimer) {
|
||||||
|
clearTimeout(bgmChimeTimer);
|
||||||
|
bgmChimeTimer = null;
|
||||||
|
}
|
||||||
|
if (bgmGlideTimer) {
|
||||||
|
clearTimeout(bgmGlideTimer);
|
||||||
|
bgmGlideTimer = null;
|
||||||
|
}
|
||||||
|
if (!bgmCtx || !bgmGain) return;
|
||||||
|
const g = bgmGain;
|
||||||
|
const nodes = bgmNodes;
|
||||||
|
const now = bgmCtx.currentTime;
|
||||||
|
g.gain.cancelScheduledValues(now);
|
||||||
|
g.gain.setValueAtTime(g.gain.value, now);
|
||||||
|
g.gain.linearRampToValueAtTime(0, now + 1.5);
|
||||||
|
bgmGain = null;
|
||||||
|
bgmNodes = [];
|
||||||
|
setTimeout(() => {
|
||||||
|
nodes.forEach((n) => { try { n.stop(); } catch { /* 已停止 */ } });
|
||||||
|
g.disconnect();
|
||||||
|
}, 1700);
|
||||||
|
}
|
||||||
|
|
||||||
let sutra = null; // 当前经
|
let sutra = null; // 当前经
|
||||||
let segments = []; // 当前经分段
|
let segments = []; // 当前经分段
|
||||||
let currentPage = 0;
|
let currentPage = 0;
|
||||||
@@ -286,6 +557,7 @@ function stopPlayback() {
|
|||||||
audioEl.pause();
|
audioEl.pause();
|
||||||
audioEl.ontimeupdate = null;
|
audioEl.ontimeupdate = null;
|
||||||
audioEl.onended = null;
|
audioEl.onended = null;
|
||||||
|
stopBgm();
|
||||||
clearHighlight();
|
clearHighlight();
|
||||||
currentSeg = -1;
|
currentSeg = -1;
|
||||||
}
|
}
|
||||||
@@ -440,10 +712,12 @@ async function playFrom(index) {
|
|||||||
audioEl.onended = () => {
|
audioEl.onended = () => {
|
||||||
setPlayingUI(false);
|
setPlayingUI(false);
|
||||||
setStatus("诵读完毕");
|
setStatus("诵读完毕");
|
||||||
|
stopBgm();
|
||||||
clearHighlight();
|
clearHighlight();
|
||||||
};
|
};
|
||||||
audioEl.currentTime = built.times[index];
|
audioEl.currentTime = built.times[index];
|
||||||
await audioEl.play();
|
await audioEl.play();
|
||||||
|
if (bgmOn) startBgm(); // play() 成功后起步背景音乐(用户手势上下文内)
|
||||||
setStatus(`正在朗读 · ${currentVoice()}`);
|
setStatus(`正在朗读 · ${currentVoice()}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err.name === "AbortError") return; // 已被停止/新播放取代
|
if (err.name === "AbortError") return; // 已被停止/新播放取代
|
||||||
@@ -462,6 +736,16 @@ btnLoop.addEventListener("click", () => {
|
|||||||
setStatus(loopMode ? "循环诵读:开" : "循环诵读:关");
|
setStatus(loopMode ? "循环诵读:开" : "循环诵读:关");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/** 背景音乐开关:播放中开/关即时淡入淡出;未播放时只记录状态,下次播放生效。 */
|
||||||
|
btnBgm.addEventListener("click", () => {
|
||||||
|
bgmOn = !bgmOn;
|
||||||
|
btnBgm.classList.toggle("active", bgmOn);
|
||||||
|
btnBgm.setAttribute("aria-pressed", String(bgmOn));
|
||||||
|
if (bgmOn && playing) startBgm();
|
||||||
|
if (!bgmOn) stopBgm();
|
||||||
|
setStatus(bgmOn ? "背景音乐:开" : "背景音乐:关");
|
||||||
|
});
|
||||||
|
|
||||||
btnPlay.addEventListener("click", () => {
|
btnPlay.addEventListener("click", () => {
|
||||||
if (playing || inflight) {
|
if (playing || inflight) {
|
||||||
stopPlayback();
|
stopPlayback();
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# 部署佛经诵读站到 fj.all8ai.top(154.8.162.18:/opt/fj/)
|
||||||
|
#
|
||||||
|
# 用法:
|
||||||
|
# ./deploy.sh # 已配置 SSH key 时直接用
|
||||||
|
# FJ_PASS=xxx ./deploy.sh # 密码登录(需本机装 sshpass)
|
||||||
|
#
|
||||||
|
# 注意:不要在脚本里写死密码;密码走 FJ_PASS 环境变量。
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
|
HOST=154.8.162.18
|
||||||
|
USER=root
|
||||||
|
DEST=/opt/fj
|
||||||
|
# 注意:不同步 .env —— 服务器端 PORT=8018 与本地开发配置不同,避免覆盖。
|
||||||
|
FILES=(index.html style.css app.js server.py icon.svg icon-180.png README.md run.md)
|
||||||
|
|
||||||
|
SSH_OPTS="-o StrictHostKeyChecking=no -o ConnectTimeout=15"
|
||||||
|
SSH_CMD="ssh $SSH_OPTS"
|
||||||
|
if [ -n "${FJ_PASS:-}" ]; then
|
||||||
|
command -v sshpass >/dev/null || { echo "缺少 sshpass:brew install sshpass 或改用 SSH key"; exit 1; }
|
||||||
|
SSH_CMD="sshpass -p $FJ_PASS ssh $SSH_OPTS"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "==> 同步文件到 $USER@$HOST:$DEST/"
|
||||||
|
rsync -az --timeout=30 -e "$SSH_CMD" "${FILES[@]}" "$USER@$HOST:$DEST/"
|
||||||
|
|
||||||
|
echo "==> 重启 fj.service"
|
||||||
|
$SSH_CMD "$USER@$HOST" 'systemctl restart fj.service && sleep 1 && systemctl is-active fj.service'
|
||||||
|
|
||||||
|
echo "==> 健康检查"
|
||||||
|
code=$(curl -s -o /dev/null -w "%{http_code}" --max-time 15 https://fj.all8ai.top/)
|
||||||
|
echo "https://fj.all8ai.top/ -> $code"
|
||||||
|
[ "$code" = "200" ] || { echo "健康检查未通过"; exit 1; }
|
||||||
|
echo "部署完成"
|
||||||
@@ -53,6 +53,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="tool-group" role="group" aria-label="模式">
|
<div class="tool-group" role="group" aria-label="模式">
|
||||||
<button type="button" id="btn-loop" class="layout-btn" aria-pressed="false">循环</button>
|
<button type="button" id="btn-loop" class="layout-btn" aria-pressed="false">循环</button>
|
||||||
|
<button type="button" id="btn-bgm" class="layout-btn" aria-pressed="false">配乐</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="status" class="status" role="status"></div>
|
<div id="status" class="status" role="status"></div>
|
||||||
<button type="button" id="btn-expand" class="expand-btn" aria-expanded="false" aria-label="展开设置" title="设置">
|
<button type="button" id="btn-expand" class="expand-btn" aria-expanded="false" aria-label="展开设置" title="设置">
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ python3 server.py
|
|||||||
- 服务器:`154.8.162.18`(OpenCloudOS 9.4),应用目录 `/opt/fj`
|
- 服务器:`154.8.162.18`(OpenCloudOS 9.4),应用目录 `/opt/fj`
|
||||||
- 进程:`systemctl status|restart fj`(`/etc/systemd/system/fj.service`,Restart=always)
|
- 进程:`systemctl status|restart fj`(`/etc/systemd/system/fj.service`,Restart=always)
|
||||||
- 反代:`/etc/nginx/conf.d/fj.conf` → `http://127.0.0.1:8018`(与 gugong/bs 等站点并存,互不冲突)
|
- 反代:`/etc/nginx/conf.d/fj.conf` → `http://127.0.0.1:8018`(与 gugong/bs 等站点并存,互不冲突)
|
||||||
- 环境变量:服务器上 `/opt/fj/.env`(`PORT=8018`、密钥等,不入 Git)
|
- 环境变量:服务器上 `/opt/fj/.env`(`PORT=8018`、密钥等,不入 Git,也不被 deploy.sh 覆盖)
|
||||||
- 更新发布:本地改完后 `rsync -avz --exclude='.git' --exclude='cache' index.html style.css app.js server.py .env root@154.8.162.18:/opt/fj/`,如有 Python 改动再 `systemctl restart fj`;纯前端改动刷新即可
|
- 更新发布:一键 `./deploy.sh`(同步前端+`server.py` → 重启 `fj.service` → 健康检查 https://fj.all8ai.top)。密码登录用 `FJ_PASS=xxx ./deploy.sh`(需本机 sshpass);已配 SSH key 则直接 `./deploy.sh`。**注意**:`.env` 不在同步列表内,服务器端 `PORT=8018` 不会被本地 `PORT=8000` 覆盖
|
||||||
|
|
||||||
备份:`cache/` 可删(自动重建);`/opt/fj` 目录 + `fj.service` + `fj.conf` 为全部恢复所需。
|
备份:`cache/` 可删(自动重建);`/opt/fj` 目录 + `fj.service` + `fj.conf` 为全部恢复所需。
|
||||||
|
|||||||
Reference in New Issue
Block a user