feat: 新增5部经典下拉选经、TTS重试、PWA可安装
- 新增阿弥陀经/普门品/道德经/清静经/太上感应篇(维基文库底本,OpenCC转简体) - 经典切换改为下拉框(佛/道分组),移动端复用底部面板并显示分组标题 - TTS 请求失败自动重试3次;普门品偈颂拆分修复600字上限问题 - 服务端缓存原子写(.tmp+rename),上限放宽至800字 - PWA:manifest + Service Worker,安装后无地址栏独立窗口 - 偏好记忆/逐字光标/回到诵读处/夜间模式等此前改动一并入库 Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
@@ -10,6 +10,7 @@ const SUTRAS = [
|
||||
name: "心经",
|
||||
title: "般若波罗蜜多心经",
|
||||
seal: "心经",
|
||||
cat: "佛经",
|
||||
translator: "唐三藏法师 玄奘 译",
|
||||
source: "故宫博物院",
|
||||
sourceUrl: "https://www.dpm.org.cn/lemmas/243222.html",
|
||||
@@ -34,6 +35,7 @@ const SUTRAS = [
|
||||
name: "金刚经",
|
||||
title: "金刚般若波罗蜜经",
|
||||
seal: "金刚经",
|
||||
cat: "佛经",
|
||||
translator: "姚秦三藏法师 鸠摩罗什 译",
|
||||
source: "维基文库 · 大正新修大藏经",
|
||||
sourceUrl: "https://zh.wikisource.org/wiki/金剛般若波羅蜜經_(鳩摩羅什)",
|
||||
@@ -77,6 +79,208 @@ const SUTRAS = [
|
||||
{ h: "应化非真分第三十二", g: "应现设化,亦非真实", t: "“须菩提!若有人以满无量阿僧祇世界七宝,持用布施;若有善男子、善女人,发菩提心者,持于此经,乃至四句偈等,受持读诵,为人演说,其福胜彼。云何为人演说?不取于相,如如不动。何以故?一切有为法,如梦幻泡影,如露亦如电,应作如是观。”佛说是经已,长老须菩提及诸比丘、比丘尼、优婆塞、优婆夷、一切世间天、人、阿修罗,闻佛所说,皆大欢喜,信受奉行。" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "amitabha",
|
||||
name: "阿弥陀经",
|
||||
title: "佛说阿弥陀经",
|
||||
seal: "弥陀经",
|
||||
cat: "佛经",
|
||||
translator: "姚秦三藏法师 鸠摩罗什 译",
|
||||
source: "维基文库 · 大正新修大藏经",
|
||||
sourceUrl: "https://zh.wikisource.org/wiki/佛說阿彌陀經",
|
||||
pageSize: 5, // 每页 5 段
|
||||
segments: [
|
||||
{ t: "如是我闻:一时,佛在舍卫国衹树给孤独园,与大比丘僧千二百五十人俱,皆是大阿罗汉,众所知识。长老舍利弗、摩诃目犍连、摩诃迦叶、摩诃迦旃延、摩诃俱𫄨罗、离婆多、周利槃陀伽、难陀、阿难陀、罗睺罗、憍梵波提、宾头卢颇罗堕、迦留陀夷、摩诃劫宾那、薄拘罗、阿那楼驮,如是等诸大弟子。并诸菩萨摩诃萨——文殊师利法王子、阿逸多菩萨、干陀诃提菩萨、常精进菩萨——与如是等诸大菩萨,及释提桓因等无量诸天大众俱。" },
|
||||
{ t: "尔时,佛告长老舍利弗:「从是西方过十万亿佛土,有世界名曰极乐。其土有佛,号阿弥陀,今现在说法。舍利弗!彼土何故名为极乐?其国众生无有众苦,但受诸乐,故名极乐。又舍利弗!极乐国土,七重栏楯、七重罗网、七重行树,皆是四宝周匝围绕,是故彼国名为极乐。" },
|
||||
{ t: "又舍利弗!极乐国土有七宝池,八功德水充满其中,池底纯以金沙布地。四边阶道,金、银、琉璃、玻璃合成;上有楼阁,亦以金、银、琉璃、玻璃、砗磲、赤珠、玛瑙而严饰之。池中莲华,大如车轮,青色青光,黄色黄光,赤色赤光,白色白光,微妙香洁。舍利弗!极乐国土成就如是功德庄严。" },
|
||||
{ t: "又舍利弗!彼佛国土,常作天乐,黄金为地,昼夜六时,雨天曼陀罗华。其土众生,常以清旦,各以衣裓盛众妙华,供养他方十万亿佛;即以食时,还到本国,饭食经行。舍利弗!极乐国土成就如是功德庄严。" },
|
||||
{ t: "复次舍利弗!彼国常有种种奇妙杂色之鸟——白鹤、孔雀、鹦鹉、舍利、迦陵频伽、共命之鸟。是诸众鸟,昼夜六时出和雅音,其音演畅五根、五力、七菩提分、八圣道分如是等法。其土众生闻是音已,皆悉念佛、念法、念僧。舍利弗!汝勿谓此鸟实是罪报所生。所以者何?彼佛国土无三恶道。舍利弗!其佛国土尚无三恶道之名,何况有实?是诸众鸟,皆是阿弥陀佛欲令法音宣流,变化所作。舍利弗!彼佛国土微风吹动,诸宝行树及宝罗网出微妙音,譬如百千种乐同时俱作,闻是音者,自然皆生念佛念法念僧之心。舍利弗!其佛国土成就如是功德庄严。" },
|
||||
{ t: "舍利弗,于汝意云何?彼佛何故号阿弥陀?舍利弗!彼佛光明无量,照十方国无所障碍,是故号为阿弥陀。又舍利弗!彼佛寿命及其人民,无量无边阿僧祇劫,故名阿弥陀。舍利弗!阿弥陀佛成佛以来,于今十劫。又舍利弗!彼佛有无量无边声闻弟子,皆阿罗汉,非是算数之所能知;诸菩萨众,亦复如是。舍利弗!彼佛国土成就如是功德庄严。" },
|
||||
{ t: "又舍利弗!极乐国土众生生者,皆是阿鞞跋致。其中多有一生补处,其数甚多,非是算数所能知之,但可以无量无边阿僧祇说。舍利弗!众生闻者,应当发愿,愿生彼国。所以者何?得与如是诸上善人俱会一处。" },
|
||||
{ t: "舍利弗!不可以少善根福德因缘得生彼国。舍利弗!若有善男子、善女人,闻说阿弥陀佛,执持名号,若一日、若二日、若三日、若四日、若五日、若六日、若七日,一心不乱。其人临命终时,阿弥陀佛与诸圣众,现在其前。是人终时,心不颠倒,即得往生阿弥陀佛极乐国土。舍利弗!我见是利,故说此言。若有众生闻是说者,应当发愿生彼国土。" },
|
||||
{ t: "舍利弗!如我今者,赞叹阿弥陀佛不可思议功德之利;东方亦有阿閦鞞佛、须弥相佛、大须弥佛、须弥光佛、妙音佛,如是等恒河沙数诸佛,各于其国出广长舌相,遍覆三千大千世界,说诚实言:『汝等众生,当信是称赞不可思议功德一切诸佛所护念经。』" },
|
||||
{ t: "舍利弗!南方世界有日月灯佛、名闻光佛、大燄肩佛、须弥灯佛、无量精进佛,如是等恒河沙数诸佛,各于其国出广长舌相,遍覆三千大千世界,说诚实言:『汝等众生,当信是称赞不可思议功德一切诸佛所护念经。』" },
|
||||
{ t: "舍利弗!西方世界有无量寿佛、无量相佛、无量幢佛、大光佛、大明佛、宝相佛、净光佛,如是等恒河沙数诸佛,各于其国出广长舌相,遍覆三千大千世界,说诚实言:『汝等众生,当信是称赞不可思议功德一切诸佛所护念经。』" },
|
||||
{ t: "舍利弗!北方世界有燄肩佛、最胜音佛、难沮佛、日生佛、网明佛,如是等恒河沙数诸佛,各于其国出广长舌相,遍覆三千大千世界,说诚实言:『汝等众生,当信是称赞不可思议功德一切诸佛所护念经。』" },
|
||||
{ t: "舍利弗!下方世界有师子佛、名闻佛、名光佛、达摩佛、法幢佛、持法佛,如是等恒河沙数诸佛,各于其国出广长舌相,遍覆三千大千世界,说诚实言:『汝等众生,当信是称赞不可思议功德一切诸佛所护念经。』" },
|
||||
{ t: "舍利弗!上方世界有梵音佛、宿王佛、香上佛、香光佛、大燄肩佛、杂色宝华严身佛、娑罗树王佛、宝华德佛、见一切义佛、如须弥山佛,如是等恒河沙数诸佛,各于其国出广长舌相,遍覆三千大千世界,说诚实言:『汝等众生,当信是称赞不可思议功德一切诸佛所护念经。』" },
|
||||
{ t: "舍利弗!于汝意云何?何故名为『一切诸佛所护念经』?舍利弗!若有善男子、善女人,闻是经受持者,及闻诸佛名者;是诸善男子、善女人,皆为一切诸佛之所护念,皆得不退转于阿耨多罗三藐三菩提。是故舍利弗!汝等皆当信受我语及诸佛所说。" },
|
||||
{ t: "舍利弗!若有人已发愿、今发愿、当发愿,欲生阿弥陀佛国者;是诸人等,皆得不退转于阿耨多罗三藐三菩提,于彼国土若已生、若今生、若当生。是故舍利弗!诸善男子、善女人若有信者,应当发愿生彼国土。" },
|
||||
{ t: "舍利弗!如我今者,称赞诸佛不可思议功德;彼诸佛等,亦称赞我不可思议功德,而作是言:『释迦牟尼佛能为甚难希有之事,能于娑婆国土五浊恶世,劫浊、见浊、烦恼浊、众生浊、命浊中,得阿耨多罗三藐三菩提,为诸众生说是一切世间难信之法。』舍利弗!当知我于五浊恶世行此难事,得阿耨多罗三藐三菩提,为一切世间说此难信之法,是为甚难!」" },
|
||||
{ t: "佛说此经已,舍利弗及诸比丘,一切世间天、人、阿修罗等,闻佛所说,欢喜信受,作礼而去。" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "pumen",
|
||||
name: "普门品",
|
||||
title: "观世音菩萨普门品",
|
||||
seal: "普门品",
|
||||
cat: "佛经",
|
||||
translator: "姚秦鸠摩罗什 译长行 · 隋阇那崛多 译重颂",
|
||||
source: "维基文库 · 大正新修大藏经",
|
||||
sourceUrl: "https://zh.wikisource.org/wiki/妙法蓮華經觀世音菩薩普門品經",
|
||||
pageSize: 6, // 每页 6 段
|
||||
segments: [
|
||||
{ h: "妙法莲华经观世音菩萨普门品第二十五", g: "姚秦鸠摩罗什译长行 · 隋阇那崛多译重颂", t: "尔时,无尽意菩萨即从座起,偏袒右肩,合掌向佛,而作是言:「世尊!观世音菩萨,以何因缘名观世音?」" },
|
||||
{ t: "佛告无尽意菩萨:「善男子!若有无量百千万亿众生受诸苦恼,闻是观世音菩萨,一心称名,观世音菩萨即时观其音声,皆得解脱。若有持是观世音菩萨名者,设入大火,火不能烧,由是菩萨威神力故。若为大水所漂,称其名号,即得浅处。若有百千万亿众生,为求金、银、琉璃、砗磲、玛瑙、珊瑚、琥珀、真珠等宝,入于大海,假使黑风吹其船舫,飘堕罗刹鬼国,其中若有,乃至一人,称观世音菩萨名者,是诸人等皆得解脱罗刹之难。以是因缘,名观世音。若复有人临当被害,称观世音菩萨名者,彼所执刀杖寻段段坏,而得解脱。若三千大千国土,满中夜叉、罗刹,欲来恼人,闻其称观世音菩萨名者,是诸恶鬼,尚不能以恶眼视之,况复加害。设复有人,若有罪、若无罪,杻械、枷锁检系其身,称观世音菩萨名者,皆悉断坏,即得解脱。若三千大千国土,满中怨贼,有一商主,将诸商人,赍持重宝、经过险路,其中一人作是唱言:『诸善男子!勿得恐怖,汝等应当一心称观世音菩萨名号。是菩萨能以无畏施于众生,汝等若称名者,于此怨贼当得解脱。』众商人闻,俱发声言:『南无观世音菩萨。』称其名故,即得解脱。" },
|
||||
{ t: "「无尽意!观世音菩萨摩诃萨,威神之力巍巍如是。若有众生多于淫欲,常念恭敬观世音菩萨,便得离欲。若多瞋恚,常念恭敬观世音菩萨,便得离瞋。若多愚痴,常念恭敬观世音菩萨,便得离痴。" },
|
||||
{ t: "「无尽意!观世音菩萨有如是等大威神力,多所饶益,是故众生常应心念。若有女人,设欲求男,礼拜供养观世音菩萨,便生福德智慧之男;设欲求女,便生端正有相之女。宿殖德本,众人爱敬。" },
|
||||
{ t: "「无尽意!观世音菩萨有如是力,若有众生,恭敬礼拜观世音菩萨,福不唐捐,是故众生皆应受持观世音菩萨名号。" },
|
||||
{ t: "「无尽意!若有人受持六十二亿恒河沙菩萨名字,复尽形供养饮食、衣服、卧具、医药。于汝意云何?是善男子、善女人,功德多不?」" },
|
||||
{ t: "无尽意言:「甚多,世尊!」" },
|
||||
{ t: "佛言:「若复有人受持观世音菩萨名号,乃至一时礼拜、供养,是二人福,正等无异,于百千万亿劫不可穷尽。无尽意!受持观世音菩萨名号,得如是无量无边福德之利。」" },
|
||||
{ t: "无尽意菩萨白佛言:「世尊!观世音菩萨,云何游此娑婆世界?云何而为众生说法?方便之力,其事云何?」" },
|
||||
{ t: "佛告无尽意菩萨:「善男子!若有国土众生,应以佛身得度者,观世音菩萨即现佛身而为说法;应以辟支佛身得度者,即现辟支佛身而为说法;应以声闻身得度者,即现声闻身而为说法;应以梵王身得度者,即现梵王身而为说法;应以帝释身得度者,即现帝释身而为说法;应以自在天身得度者,即现自在天身而为说法;应以大自在天身得度者,即现大自在天身而为说法;应以天大将军身得度者,即现天大将军身而为说法;应以毗沙门身得度者,即现毗沙门身而为说法;应以小王身得度者,即现小王身而为说法;应以长者身得度者,即现长者身而为说法;应以居士身得度者,即现居士身而为说法;应以宰官身得度者,即现宰官身而为说法;应以婆罗门身得度者,即现婆罗门身而为说法;应以比丘、比丘尼、优婆塞、优婆夷身得度者,即现比丘、比丘尼、优婆塞、优婆夷身而为说法;应以长者、居士、宰官、婆罗门妇女身得度者,即现妇女身而为说法;应以童男、童女身得度者,即现童男、童女身而为说法;应以天、龙、夜叉、干闼婆、阿修罗、迦楼罗、紧那罗、摩睺罗伽、人非人等身得度者,即皆现之而为说法;应以执金刚身得度者,即现执金刚身而为说法。" },
|
||||
{ t: "「无尽意!是观世音菩萨成就如是功德,以种种形,游诸国土,度脱众生。是故汝等,应当一心供养观世音菩萨。是观世音菩萨摩诃萨,于怖畏急难之中能施无畏,是故此娑婆世界,皆号之为施无畏者。」" },
|
||||
{ t: "无尽意菩萨白佛言:「世尊!我今当供养观世音菩萨。」即解颈众宝珠、璎珞,价值百千两金,而以与之,作是言:「仁者!受此法施珍宝璎珞。」时观世音菩萨不肯受之。无尽意复白观世音菩萨言:「仁者!愍我等故,受此璎珞。」" },
|
||||
{ t: "尔时佛告观世音菩萨:「当愍此无尽意菩萨及四众,天、龙、夜叉、干闼婆、阿修罗、迦楼罗、紧那罗、摩睺罗伽、人非人等故,受是璎珞。」" },
|
||||
{ t: "即时观世音菩萨愍诸四众,及于天、龙、人非人等,受其璎珞,分作二分,一分奉释迦牟尼佛,一分奉多宝佛塔。" },
|
||||
{ t: "「无尽意,观世音菩萨有如是自在神力,游于娑婆世界。」" },
|
||||
{ t: "尔时无尽意菩萨以偈问曰:" },
|
||||
{ t: "世尊妙相具,我今重问彼,佛子何因缘,名为观世音。具足妙相尊,偈答无尽意。汝听观音行,善应诸方所,弘誓深如海,历劫不思议,侍多千亿佛,发大清净愿。我为汝略说,闻名及见身,心念不空过,能灭诸有苦。假使兴害意,推落大火坑,念彼观音力,火坑变成池。或漂流巨海,龙鱼诸鬼难,念彼观音力,波浪不能没。或在须弥峰、为人所推堕,念彼观音力,如日虚空住。或被恶人逐,堕落金刚山,念彼观音力,不能损一毛。或值怨贼绕,各执刀加害,念彼观音力,咸即起慈心。或遭王难苦,临刑欲寿终,念彼观音力,刀寻段段坏。或囚禁枷锁,手足被杻械,念彼观音力,释然得解脱。咒诅诸毒药、所欲害身者,念彼观音力,还著于本人。或遇恶罗刹、毒龙诸鬼等,念彼观音力,时悉不敢害。若恶兽围绕,利牙爪可怖,念彼观音力,疾走无边方。蚖蛇及蝮蝎,气毒烟火燃,念彼观音力,寻声自回去。云雷鼓掣电,降雹澍大雨,念彼观音力,应时得消散。" },
|
||||
{ t: "众生被困厄,无量苦逼身,观音妙智力,能救世间苦。具足神通力,广修智方便,十方诸国土,无刹不现身。种种诸恶趣,地狱鬼畜生,生老病死苦,以渐悉令灭。真观清净观,广大智慧观,悲观及慈观,常愿常瞻仰。无垢清净光、慧日破诸暗,能伏灾风火,普明照世间。悲体戒雷震,慈意妙大云,澍甘露法雨,灭除烦恼焰。诤讼经官处,怖畏军阵中,念彼观音力,众怨悉退散。妙音观世音、梵音海潮音,胜彼世间音,是故须常念,念念勿生疑。观世音净圣,于苦恼死厄、能为作依怙。具一切功德,慈眼视众生,福聚海无量,是故应顶礼。" },
|
||||
{ t: "尔时持地菩萨即从座起,前白佛言:「世尊!若有众生,闻是观世音菩萨品自在之业,普门示现神通力者,当知是人功德不少。」" },
|
||||
{ t: "佛说是普门品时,众中八万四千众生,皆发无等等阿耨多罗三藐三菩提心。" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "daodejing",
|
||||
name: "道德经",
|
||||
title: "道德经",
|
||||
seal: "道德经",
|
||||
cat: "道家",
|
||||
translator: "春秋 · 老子 著(王弼本)",
|
||||
source: "维基文库",
|
||||
sourceUrl: "https://zh.wikisource.org/wiki/道德經_(王弼本)",
|
||||
pageSize: 9, // 81 章,每页 9 章
|
||||
segments: [
|
||||
{ h: "第一章", t: "道可道,非常道,名可名,非常名;无名,天地之始,有名,万物之母。故常无欲,以观其妙,常有欲,以观其徼;此两者,同出而异名,同谓之玄。玄之又玄,众妙之门。" },
|
||||
{ h: "第二章", t: "天下皆知美之为美,斯恶已。皆知善之为善,斯不善已。故有无相生,难易相成,长短相较,高下相倾,音声相和,前后相随。是以圣人处无为之事,行不言之教;万物作焉而不辞,生而不有,为而不恃,功成而弗居。夫唯弗居,是以不去。" },
|
||||
{ h: "第三章", t: "不尚贤,使民不争;不贵难得之货,使民不为盗;不见可欲,使民心不乱。是以圣人之治,虚其心,实其腹,弱其志,强其骨。常使民无知无欲。使夫智者不敢为也。为无为,则无不治。" },
|
||||
{ h: "第四章", t: "道冲而用之或不盈,渊兮似万物之宗;挫其锐,解其纷,和其光,同其尘,湛兮似或存。吾不知谁之子,象帝之先。" },
|
||||
{ h: "第五章", t: "天地不仁,以万物为刍狗;圣人不仁,以百姓为刍狗。天地之间,其犹橐籥乎?虚而不屈,动而愈出。多言数穷,不如守中。" },
|
||||
{ h: "第六章", t: "谷神不死,是谓玄牝。玄牝之门,是谓天地根。緜緜若存,用之不勤。" },
|
||||
{ h: "第七章", t: "天长地久。天地所以能长且久者,以其不自生,故能长生。是以圣人后其身而身先;外其身而身存。非以其无私邪,故能成其私。" },
|
||||
{ h: "第八章", t: "上善若水。水善利万物而不争,处众人之所恶,故几于道。居善地,心善渊,与善仁,言善信,正善治,事善能,动善时。夫唯不争,故无尤。" },
|
||||
{ h: "第九章", t: "持而盈之,不如其已;揣而棁之,不可长保。金玉满堂,莫之能守;富贵而骄,自遗其咎。功遂身退,天之道。" },
|
||||
{ h: "第十章", t: "载营魄抱一,能无离乎?专气致柔,能婴儿乎?涤除玄览,能无疵乎?爱国治民,能无知乎?天门开阖,能为雌乎?明白四达,能无为乎?生之,畜之。生而不有,为而不恃,长而不宰,是谓玄德。" },
|
||||
{ h: "第十一章", t: "三十辐共一毂,当其无,有车之用。埏埴以为器,当其无,有器之用。凿户牖以为室,当其无,有室之用。故有之以为利,无之以为用。" },
|
||||
{ h: "第十二章", t: "五色令人目盲,五音令人耳聋,五味令人口爽,驰骋畋猎令人心发狂,难得之货令人行妨。是以圣人为腹不为目,故去彼取此。" },
|
||||
{ h: "第十三章", t: "宠辱若惊,贵大患若身。何谓宠辱若惊?宠为下,得之若惊,失之若惊,是谓宠辱若惊。何谓贵大患若身?吾所以有大患者,为吾有身,及吾无身,吾有何患?故贵以身为天下,若可寄天下;爱以身为天下,若可托天下。" },
|
||||
{ h: "第十四章", t: "视之不见名曰夷,听之不闻名曰希,搏之不得名曰微。此三者,不可致诘,故混而为一。其上不皦,其下不昧。绳绳不可名,复归于无物。是谓无状之状,无物之象,是谓惚恍。迎之不见其首,随之不见其后。执古之道,以御今之有。能知古始,是谓道纪。" },
|
||||
{ h: "第十五章", t: "古之善为士者,微妙玄通,深不可识。夫唯不可识,故强为之容。豫焉若冬涉川,犹兮若畏四邻,俨兮其若容,涣兮若冰之将释,敦兮其若朴,旷兮其若谷,混兮其若浊。孰能浊以静之徐清?孰能安以久动之徐生?保此道者不欲盈,夫唯不盈,故能蔽不新成。" },
|
||||
{ h: "第十六章", t: "致虚极,守静笃。万物并作,吾以观复。夫物芸芸,各复归其根。归根曰静,是谓复命。复命曰常,知常曰明。不知常,妄作凶。知常容,容乃公,公乃全,全乃天,天乃道,道乃久,没身不殆。" },
|
||||
{ h: "第十七章", t: "太上,下知有之,其次,亲而誉之,其次,畏之,其次,侮之。信不足焉,有不信焉。悠兮其贵言,功成事遂,百姓皆谓:我自然。" },
|
||||
{ h: "第十八章", t: "大道废,有仁义;智慧出,有大伪;六亲不和,有孝慈;国家昏乱,有忠臣。" },
|
||||
{ h: "第十九章", t: "绝圣弃智,民利百倍;绝仁弃义,民复孝慈;绝巧弃利,盗贼无有。此三者以为文不足,故令有所属﹕见素抱朴,少私寡欲。" },
|
||||
{ h: "第二十章", t: "绝学无忧,唯之与阿,相去几何?善之与恶,相去若何?人之所畏,不可不畏。荒兮其未央哉﹗众人熙熙,如享太牢,如春登台。我独泊兮其未兆,如婴儿之未孩;儽儽兮若无所归。众人皆有余,而我独若遗。我愚人之心也哉﹗沌沌兮,俗人昭昭,我独昏昏。俗人察察,我独闷闷。澹兮其若海,飂兮若无止。众人皆有以,而我独顽似鄙。我独异于人,而贵食母。" },
|
||||
{ h: "第二十一章", t: "孔德之容,惟道是从。道之为物,惟恍惟惚。惚兮恍兮,其中有象;恍兮惚兮,其中有物。窈兮冥兮,其中有精;其精甚真,其中有信。自今及古,其名不去,以阅众甫。吾何以知众甫之状哉?以此。" },
|
||||
{ h: "第二十二章", t: "曲则全,枉则直,洼则盈,敝则新,少则得,多则惑。是以圣人抱一为天下式。不自见故明,不自是故彰,不自伐故有功,不自矜故长。夫唯不争,故天下莫能与之争。古之所谓曲则全者,岂虚言哉!诚全而归之。" },
|
||||
{ h: "第二十三章", t: "希言自然。故飘风不终朝,骤雨不终日。孰为此者?天地。天地尚不能久,而况于人乎?故从事于道者,道者同于道,德者同于德,失者同于失。同于道者,道亦乐得之;同于德者,德亦乐得之;同于失者,失亦乐得之。信不足焉,有不信焉。" },
|
||||
{ h: "第二十四章", t: "企者不立,跨者不行,自见者不明,自是者不彰,自伐者无功,自矜者不长。其在道也,曰余食赘行。物或恶之,故有道者不处。" },
|
||||
{ h: "第二十五章", t: "有物混成,先天地生。寂兮寥兮,独立而不改,周行而不殆,可以为天下母。吾不知其名,字之曰道,强为之名曰大。大曰逝,逝曰远,远曰反。故道大,天大,地大,王亦大。域中有四大,而王居其一焉。人法地,地法天,天法道,道法自然。" },
|
||||
{ h: "第二十六章", t: "重为轻根,静为躁君。是以圣人终日行不离辎重。虽有荣观,燕处超然。奈何万乘之主,而以身轻天下?轻则失本,躁则失君。" },
|
||||
{ h: "第二十七章", t: "善行无辙迹,善言无瑕讁;善数不用筹策;善闭无关楗而不可开,善结无绳约而不可解。是以圣人常善救人,故无弃人;常善救物,故无弃物,是谓袭明。故善人者,不善人之师;不善人者,善人之资。不贵其师,不爱其资,虽智大迷,是谓要妙。" },
|
||||
{ h: "第二十八章", t: "知其雄,守其雌,为天下谿。为天下谿,常德不离,复归于婴儿。知其白,守其黑,为天下式。为天下式,常德不忒,复归于无极。知其荣,守其辱,为天下谷,常德乃足,复归于朴。朴散则为器,圣人用之,则为官长,故大制不割。" },
|
||||
{ h: "第二十九章", t: "将欲取天下而为之,吾见其不得已。天下神器,不可为也,为者败之,执者失之。故物或行或随,或歔或吹。或强或羸,或挫或隳。是以圣人去甚,去奢,去泰。" },
|
||||
{ h: "第三十章", t: "以道佐人主者,不以兵强天下。其事好还。师之所处,荆棘生焉。大军之后,必有凶年。善有果而已,不敢以取强。果而勿矜,果而勿伐,果而勿骄。果而不得已,果而勿强。物壮则老,是谓不道,不道早已。" },
|
||||
{ h: "第三十一章", t: "夫佳兵者,不祥之器,物或恶之,故有道者不处。君子居则贵左,用兵则贵右。兵者不祥之器,非君子之器,不得已而用之,恬淡为上。胜而不美,而美之者,是乐杀人。夫乐杀人者,则不可以得志于天下矣。吉事尚左,凶事尚右。偏将军居左,上将军居右,言以丧礼处之。杀人之众,以哀悲泣之,战胜,以丧礼处之。" },
|
||||
{ h: "第三十二章", t: "道常无名,朴虽小,天下莫能臣也。侯王若能守之,万物将自宾。天地相合,以降甘露,民莫之令而自均。始制有名,名亦既有,夫亦将知止,知止所以不殆。譬道之在天下,犹川谷之于江海。" },
|
||||
{ h: "第三十三章", t: "知人者智,自知者明。胜人者有力,自胜者强。知足者富。强行者有志。不失其所者久。死而不亡者寿。" },
|
||||
{ h: "第三十四章", t: "大道泛兮,其可左右。万物恃之而生而不辞,功成不名有。衣养万物而不为主,常无欲,可名于小;万物归焉而不为主,可名为大。以其终不自为大,故能成其大。" },
|
||||
{ h: "第三十五章", t: "执大象,天下往。往而不害,安平太。乐与饵,过客止。道之出口,淡乎其无味,视之不足见,听之不足闻,用之不足既。" },
|
||||
{ h: "第三十六章", t: "将欲歙之,必固张之;将欲弱之,必固强之;将欲废之,必固兴之;将欲夺之,必固与之。是谓微明。柔弱胜刚强。鱼不可脱于渊,国之利器不可以示人。" },
|
||||
{ h: "第三十七章", t: "道常无为而无不为。侯王若能守之,万物将自化。化而欲作,吾将镇之以无名之朴。无名之朴,夫亦将无欲。不欲以静,天下将自定。# 老子《道德经》下篇" },
|
||||
{ h: "第三十八章", t: "上德不德,是以有德;下德不失德,是以无德。上德无为而无以为,下德为之而有以为。上仁为之而无以为,上义为之而有以为。上礼为之而莫之应,则攘臂而扔之。故失道而后德,失德而后仁,失仁而后义,失义而后礼。夫礼者,忠信之薄,而乱之首。前识者,道之华,而愚之始。是以大丈夫处其厚,不居其薄;处其实,不居其华。故去彼取此。" },
|
||||
{ h: "第三十九章", t: "昔之得一者,天得一以清,地得一以宁,神得一以灵,谷得一以盈,万物得一以生,侯王得一以为天下贞。其致之,天无以清将恐裂,地无以宁将恐发,神无以灵将恐歇,谷无以盈将恐竭,万物无以生将恐灭,侯王无以贵高将恐蹶。故贵以贱为本,高以下为基。是以侯王自称孤﹑寡﹑不谷。此非以贱为本邪?非乎?故致数舆无舆,不欲琭琭如玉,珞珞如石。" },
|
||||
{ h: "第四十章", t: "反者道之动,弱者道之用。天下万物生于有,有生于无。" },
|
||||
{ h: "第四十一章", t: "上士闻道,勤而行之;中士闻道,若存若亡;下士闻道,大笑之。不笑不足以为道。故建言有之﹕明道若昧,进道若退,夷道若颣,上德若谷,大白若辱,广德若不足,建德若偷,质真若渝,大方无隅,大器晚成,大音希声,大象无形,道隐无名。夫唯道,善贷且成。" },
|
||||
{ h: "第四十二章", t: "道生一,一生二,二生三,三生万物。万物负阴而抱阳,冲气以为和。人之所恶,唯孤﹑寡﹑不谷,而王公以为称。故物或损之而益,或益之而损。人之所教,我亦教之。强梁者不得其死,吾将以为教父。" },
|
||||
{ h: "第四十三章", t: "天下之至柔,驰骋天下之至坚。无有入无间,吾是以知无为之有益。不言之教,无为之益,天下希及之。" },
|
||||
{ h: "第四十四章", t: "名与身孰亲?身与货孰多?得与亡孰病?是故甚爱必大费,多藏必厚亡,知足不辱,知止不殆,可以长久。" },
|
||||
{ h: "第四十五章", t: "大成若缺,其用不弊。大盈若冲,其用不穷。大直若屈,其用不居。大巧若拙,其用不辍。大辩若讷,其用不差。躁胜寒,静胜热。清静为天下正。" },
|
||||
{ h: "第四十六章", t: "天下有道,却走马以粪。天下无道,戎马生于郊。祸莫大于不知足;咎莫大于欲得。故知足之足,常足矣。" },
|
||||
{ h: "第四十七章", t: "不出户,知天下;不闚牖,见天道。其出弥远,其知弥少。是以圣人不行而知,不见而名,不为而成。" },
|
||||
{ h: "第四十八章", t: "为学日益,为道日损。损之又损,以至于无为。无为而无不为。取天下常以无事,及其有事,不足以取天下。" },
|
||||
{ h: "第四十九章", t: "圣人无常心,以百姓心为心。善者,吾善之;不善者,吾亦善之,德善。信者,吾信之;不信者,吾亦信之,德信。圣人在天下歙歙,为天下浑其心,百姓皆注其耳目, 圣人皆孩之。" },
|
||||
{ h: "第五十章", t: "出生入死。生之徒,十有三;死之徒,十有三;人之生,动之死地,亦十有三。夫何故?以其生生之厚。盖闻善摄生者,陆行不遇兕虎,入军不被甲兵;兕无所投其角,虎无所措其爪,兵无所容其刃。夫何故?以其无死地。" },
|
||||
{ h: "第五十一章", t: "道生之,德畜之,物形之,势成之。是以万物莫不尊道而贵德。道之尊,德之贵,夫莫之命而常自然。故道生之,德畜之。长之育之,亭之毒之,盖之覆之。生而不有,为而不恃,长而不宰。是谓玄德。" },
|
||||
{ h: "第五十二章", t: "天下有始,以为天下母。既得其母,以知其子,既知其子,复守其母,没身不殆。塞其兑,闭其门,终身不勤。开其兑,济其事,终身不救。见小曰明,守柔曰强。用其光,复归其明,无遗身殃,是为习常。" },
|
||||
{ h: "第五十三章", t: "使我介然有知,行于大道,唯施是畏。大道甚夷,而民好径。朝甚除,田甚芜,仓甚虚;服文彩,带利剑,厌饮食,财货有余;是为盗夸。非道也哉!" },
|
||||
{ h: "第五十四章", t: "善建者不拔,善抱者不脱,子孙以祭祀不辍。修之于身,其德乃真;修之于家,其德乃余;修之于乡,其德乃长;修之于国,其德乃丰;修之于天下,其德乃普。故以身观身,以家观家,以乡观乡,以国观国,以天下观天下。吾何以知天下然哉?以此。" },
|
||||
{ h: "第五十五章", t: "含德之厚,比于赤子。蜂虿虺蛇不螫,猛兽不据,攫鸟不搏。骨弱筋柔而握固。未知牝牡之合而全作,精之至也。终日号而不嗄,和之至也。知和曰常,知常曰明。益生曰祥。心使气曰强。物壮则老,谓之不道,不道早已。" },
|
||||
{ h: "第五十六章", t: "知者不言,言者不知。塞其兑,闭其门,挫其锐,解其分,和其光,同其尘,是谓玄同。故不可得而亲,不可得而疏;不可得而利,不可得而害;不可得而贵,不可得而贱。故为天下贵。" },
|
||||
{ h: "第五十七章", t: "以正治国,以奇用兵,以无事取天下。吾何以知其然哉?以此。天下多忌讳,而民弥贫;民多利器,国家滋昬;人多伎巧,奇物滋起;法令滋彰,盗贼多有。故圣人云﹕「我无为而民自化,我好静而民自正,我无事而民自富,我无欲而民自朴。」" },
|
||||
{ h: "第五十八章", t: "其政闷闷,其民淳淳;其政察察,其民缺缺。祸兮福之所倚,福兮祸之所伏。孰知其极?其无正。正复为奇,善复为妖。人之迷,其日固久。是以圣人方而不割,廉而不刿,直而不肆,光而不燿。" },
|
||||
{ h: "第五十九章", t: "治人事天,莫若啬。夫唯啬,是谓早服;早服谓之重积德;重积德则无不克,无不克则莫知其极;莫知其极,可以有国;有国之母,可以长久;是谓深根固柢,长生久视之道。" },
|
||||
{ h: "第六十章", t: "治大国,若烹小鲜。以道莅天下,其鬼不神;非其鬼不神,其神不伤人;非其神不伤人,圣人亦不伤人。夫两不相伤,故德交归焉。" },
|
||||
{ h: "第六十一章", t: "大国者下流,天下之交。天下之牝,牝常以静胜牡,以静为下。故大国以下小国,则取小国;小国以下大国,则取大国。故或下以取,或下而取。大国不过欲兼畜人,小国不过欲入事人。夫两者各得其所欲,大者宜为下。" },
|
||||
{ h: "第六十二章", t: "道者万物之奥。善人之宝,不善人之所保。美言可以市,尊行可以加人。人之不善,何弃之有?故立天子,置三公,虽有拱璧以先驷马,不如坐进此道。古之所以贵此道者何?不曰以求得,有罪以免邪?故为天下贵。" },
|
||||
{ h: "第六十三章", t: "为无为,事无事,味无味。大小多少,报怨以德。图难于其易,为大于其细;天下难事必作于易,天下大事必作于细。是以圣人终不为大,故能成其大。夫轻诺必寡信,多易必多难。是以圣人犹难之,故终无难矣" },
|
||||
{ h: "第六十四章", t: "其安易持,其未兆易谋。其脆易泮,其微易散。为之于未有,治之于未乱。合抱之木,生于毫末;九层之台,起于累土;千里之行,始于足下。为者败之,执者失之。是以圣人无为故无败,无执故无失。民之从事,常于几成而败之。慎终如始,则无败事。是以圣人欲不欲,不贵难得之货;学不学,复众人之所过。以辅万物之自然,而不敢为。" },
|
||||
{ h: "第六十五章", t: "古之善为道者,非以明民,将以愚之。民之难治,以其智多。故以智治国,国之贼,不以智治国,国之福。知此两者亦稽式。常知稽式,是谓玄德。玄德深矣,远矣,与物反矣,然后乃至大顺。" },
|
||||
{ h: "第六十六章", t: "江海所以能为百谷王者,以其善下之,故能为百谷王。是以欲上民,必以言下之。欲先民,必以身后之。是以圣人处上而民不重,处前而民不害。是以天下乐推而不厌,以其不争,故天下莫能与之争。" },
|
||||
{ h: "第六十七章", t: "天下皆谓我道大,似不肖。夫唯大,故似不肖。若肖,久矣其细也夫!我有三宝,持而保之。一曰慈,二曰俭,三曰不敢为天下先。 慈故能勇,俭故能广,不敢为天下先,故能成器长。今舍慈且勇,舍俭且广,舍后且先,死矣! 夫慈以战则胜,以守则固。天将救之,以慈卫之。" },
|
||||
{ h: "第六十八章", t: "善为士者不武,善战者不怒,善胜敌者不与,善用人者为之下,是谓不争之德,是谓用人之力,是谓配天古之极。" },
|
||||
{ h: "第六十九章", t: "用兵有言﹕「吾不敢为主而为客,不敢进寸而退尺。」是谓行无行,攘无臂,扔无敌,执无兵。 祸莫大于轻敌,轻敌几丧吾宝。故抗兵相加,哀者胜矣。" },
|
||||
{ h: "第七十章", t: "吾言甚易知,甚易行。天下莫能知,莫能行。言有宗,事有君。夫唯无知,是以不我知。知我者希,则我者贵。是以圣人被褐怀玉。" },
|
||||
{ h: "第七十一章", t: "知不知上,不知知病。夫唯病病,是以不病。圣人不病,以其病病,是以不病。" },
|
||||
{ h: "第七十二章", t: "民不畏威,则大威至。无狎其所居,无厌其所生。夫唯不厌,是以不厌。是以圣人自知不自见,自爱不自贵;故去彼取此。" },
|
||||
{ h: "第七十三章", t: "勇于敢则杀,勇于不敢则活。此两者,或利或害。天之所恶,孰知其故?是以圣人犹难之。天之道,不争而善胜,不言而善应,不召而自来,𦈎然而善谋。天网恢恢,疏而不失。" },
|
||||
{ h: "第七十四章", t: "民不畏死,奈何以死惧之?若使民常畏死,而为奇者,吾得执而杀之,孰敢?常有司杀者杀,夫代司杀者杀,是谓代大匠斲。 夫代大匠斲者,希有不伤其手矣。" },
|
||||
{ h: "第七十五章", t: "民之饥,以其上食税之多,是以饥。民之难治,以其上之有为,是以难治。民之轻死,以其上求生之厚,是以轻死。夫唯无以生为者,是贤于贵生。" },
|
||||
{ h: "第七十六章", t: "人之生也柔弱,其死也坚强。 万物草木之生也柔脆,其死也枯槁。 故坚强者死之徒,柔弱者生之徒。是以兵强则不胜,木强则兵。强大处下,柔弱处上。" },
|
||||
{ h: "第七十七章", t: "天之道,其犹张弓与?高者抑之,下者举之;有余者损之,不足者补之。天之道,损有余而补不足。人之道则不然,损不足以奉有余。 孰能有余以奉天下?唯有道者。 是以圣人为而不恃,功成而不处,其不欲见贤。" },
|
||||
{ h: "第七十八章", t: "天下莫柔弱于水,而攻坚强者,莫之能胜,其无以易之。弱之胜强,柔之胜刚,天下莫不知莫能行。是以圣人云﹕「受国之垢,是谓社稷主;受国不祥,是为天下王。」正言若反。" },
|
||||
{ h: "第七十九章", t: "和大怨,必有余怨,安可以为善?是以圣人执左契,而不责于人。有德司契,无德司彻。天道无亲,常与善人。" },
|
||||
{ h: "第八十章", t: "小国寡民,使有什伯之器而不用,使民重死而不远徙。虽有舟舆,无所乘之;虽有甲兵,无所陈之。使人复结绳而用之,甘其食,美其服,安其居,乐其俗。邻国相望,鸡犬之声相闻,民至老死,不相往来。" },
|
||||
{ h: "第八十一章", t: "信言不美,美言不信。善者不辩,辩者不善。 知者不博,博者不知。 圣人不积,既以为人己愈有,既以与人己愈多。天之道,利而不害;圣人之道,为而不争。" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "qingjing",
|
||||
name: "清静经",
|
||||
title: "太上老君说常清静经",
|
||||
seal: "清静经",
|
||||
cat: "道家",
|
||||
translator: "正统道藏本",
|
||||
source: "维基文库 · 正统道藏",
|
||||
sourceUrl: "https://zh.wikisource.org/wiki/太上老君說常清靜經",
|
||||
pageSize: 6, // 一屏读完
|
||||
segments: [
|
||||
{ t: "老君曰:大道无形,生育天地;大道无情,运行日月;大道无名,长养万物;吾不知其名,强名曰道。夫道者:有清有浊,有动有静;天清地浊,天动地静;男清女浊,男动女静。降本流末,而生万物。清者浊之源,动者静之基。人能常清静,天地悉皆归。" },
|
||||
{ t: "夫人神好清,而心扰之;人心好静,而欲牵之。常能遣其欲而心自静,澄其心而神自清。自然六欲不生,三毒消灭。所以不能者,为心未澄,欲未遣也。能遣之者,内观于心,心无其心;外观于形,形无其形;远观于物,物无其物。三者既悟,唯见于空。观空亦空,空无所空;所空既无,无无亦无;无无既无,湛然常寂;寂无所寂,欲岂能生?欲既不生,即是真静。真常应物,真常得性;常应常静,常清静矣。如此清静,渐入真道;既入真道,名谓得道,虽名得道,实无所得;为化众生,名谓得道;能悟之者,可传圣道。" },
|
||||
{ t: "老君曰:上士无争,下士好争;上德不德,下德执德;执著之者,不名道德。众生所以不得真道者,为有妄心。既有妄心,即惊其神;既惊其神,即著万物;既著万物,即生贪求;既生贪求,即是烦恼;烦恼妄想,忧苦身心;便遭浊辱,流浪生死,常沉苦海,永失真道。真常之道,悟者自得。得悟道者,常清静矣。" },
|
||||
{ t: "仙人葛玄曰:吾得真道,曾诵此经万遍。此经是天人所习,不传下士。吾昔受之于东华帝君。东华帝君受之于金阙帝君,金阙帝君受之于西王母。西王母皆口口相传,不记文字。吾今于世书而录之。上士悟之,升为天官。中士修之,南宫列仙。下士得之,在世长年,游行三界,升入金门。" },
|
||||
{ t: "左玄真人曰:学道之士,持诵此经,即得十天善神拥护其人。然后玉符宝神,金液链形,形神俱妙,与道合真。" },
|
||||
{ t: "正一真人曰:人家有此经,悟解之者,灾障不干,众圣护门,神升上界,朝拜高尊。功满德就,相感帝君。诵持不退,身腾紫云。" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "ganying",
|
||||
name: "感应篇",
|
||||
title: "太上感应篇",
|
||||
seal: "感应篇",
|
||||
cat: "道家",
|
||||
translator: "通行本",
|
||||
source: "维基文库",
|
||||
sourceUrl: "https://zh.wikisource.org/wiki/太上感應篇",
|
||||
pageSize: 4, // 每页 4 段
|
||||
segments: [
|
||||
{ t: "太上曰:「祸福无门,惟人自召;善恶之报,如影随形。」" },
|
||||
{ t: "是以天地有司过之神,依人所犯轻重,以夺人算。算减则贫耗,多逢忧患;人皆恶之,刑祸随之,吉庆避之,恶星灾之;算尽则死。又有三台、北斗神君,在人头上,录人罪恶,夺其纪算。又有三尸神,在人身中,每到庚申日,辄上诣天曹,言人罪过。月晦之日,灶神亦然。凡人有过,大则夺纪,小则夺算。其过大小,有数百事,欲求长生者,先须避之。" },
|
||||
{ t: "是道则进,非道则退。不履邪径,不欺暗室;积德累功,慈心于物;忠孝友悌,正己化人;矜孤恤寡,敬老怀幼;昆虫草木,犹不可伤。宜悯人之凶,乐人之善;济人之急,救人之危。见人之得,如己之得;见人之失,如己之失。不彰人短,不炫己长;遏恶扬善,推多取少。受辱不怨,受宠若惊。施恩不求报,与人不追悔。" },
|
||||
{ t: "所谓善人,人皆敬之,天道佑之,福禄随之,众邪远之,神灵卫之;所作必成,神仙可冀。欲求天仙者,当立一千三百善;欲求地仙者,当立三百善。" },
|
||||
{ t: "苟或非义而动,背理而行。以恶为能,忍作残害;阴贼良善,暗侮君亲;慢其先生,叛其所事;诳诸无识,谤诸同学;虚诬诈伪,攻讦宗亲;刚强不仁,狠戾自用;是非不当,向背乖宜;虐下取功,谄上希旨;受恩不感,念怨不休;轻蔑天民,扰乱国政;赏及非义,刑及无辜;杀人取财,倾人取位;诛降戮服,贬正排贤;凌孤逼寡,弃法受赂;以直为曲,以曲为直;入轻为重,见杀加怒;知过不改,知善不为;自罪引他,壅塞方术;讪谤圣贤,侵凌道德。" },
|
||||
{ t: "射飞逐走,发蛰惊栖;填穴覆巢,伤胎破卵;愿人有失,毁人成功;危人自安,减人自益;以恶易好,以私废公,窃人之能,蔽人之善;形人之丑,讦人之私;耗人货财,离人骨肉;侵人所爱,助人为非;逞志作威,辱人求胜;败人苗稼,破人婚姻;苟富而骄,苟免无耻;认恩推过,嫁祸卖恶;沽买虚誉,包贮险心;挫人所长,护己所短;乘威迫胁,纵暴杀伤;无故剪裁,非礼烹宰;散弃五谷,劳扰众生;破人之家,取其财宝;决水放火,以害民居;紊乱规模,以败人功;损人器物,以穷人用。" },
|
||||
{ t: "见他荣贵,愿他流贬;见他富有,愿他破散;见他色美,起心私之;负他货财,愿他身死;干求不遂,便生咒恨;见他失便,便说他过;见他体相不具而笑之,见他才能可称而抑之。" },
|
||||
{ t: "埋蛊厌人,用药杀树;恚怒师傅,抵触父兄;强取强求,好侵好夺;掳掠致富,巧诈求迁;赏罚不平,逸乐过节;苛虐其下,恐吓于他;怨天尤人,呵风骂雨;鬪合争讼,妄逐朋党;用妻妾语,违父母训;得新忘故,口是心非;贪冒于财,欺罔其上;造作恶语,谗毁平人;毁人称直,骂神称正;弃顺效逆,背亲向疏;指天地以证鄙怀,引神明而鉴猥事。" },
|
||||
{ t: "施与后悔,假借不还;分外营求,力上施设;淫欲过度,心毒貌慈;秽食𫗪人,左道惑众;短尺狭度,轻秤小升;以伪杂真,采取奸利;压良为贱,谩蓦愚人;贪婪无厌,咒诅求直。" },
|
||||
{ t: "嗜酒悖乱,骨肉忿争;男不忠良,女不柔顺;不和其室,不敬其夫;每好矜夸,常行妒忌;无行于妻子,失礼于舅姑;轻慢先灵,违逆上命;作为无益,怀挟外心;自咒咒他,偏憎偏爱;越井越灶,跳食跳人;损子堕胎,行多隐僻;晦腊歌舞,朔旦号怒;对北涕唾及溺,对灶吟咏及哭;又以灶火烧香,秽柴作食;夜起裸露,八节行刑;唾流星,指虹霓;辄指三光,久视日月;春月燎猎,对北恶骂,无故杀龟打蛇……如是等罪,司命随其轻重,夺其纪算。算尽则死;死有余责,乃殃及子孙。" },
|
||||
{ t: "又诸横取人财者,乃计其妻子、家口以当之,渐至死丧。若不死丧,则有水火盗贼、遗亡器物、疾病口舌诸事,以当妄取之值。又枉杀人者,是易刀兵而相杀也。取非义之财者,譬如漏脯救饥,鸩酒止渴;非不暂饱,死亦及之。" },
|
||||
{ t: "夫心起于善,善虽未为,而吉神已随之;或心起于恶,恶虽未为,而凶神已随之。其有曾行恶事,后自改悔,诸恶莫作,众善奉行,久久必获吉庆;所谓转祸为福也。故吉人语善、视善、行善,一日有三善,三年天必降之福。凶人语恶、视恶、行恶,一日有三恶,三年天必降之祸。胡不勉而行之?" },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// ===== 可选音色(模型 qwen3-tts-flash) =====
|
||||
@@ -84,13 +288,14 @@ const VOICE_GROUPS = [
|
||||
{
|
||||
label: "诵读音色",
|
||||
voices: [
|
||||
["Seren", "Seren 塞伦 · 温润女声"],
|
||||
["Ethan", "Ethan 伊森 · 阳光男声"],
|
||||
["Seren", "Seren 塞伦 · 女声"],
|
||||
["Ethan", "Ethan 伊森 · 男声"],
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const sutraEl = document.getElementById("sutra");
|
||||
const sutraSelect = document.getElementById("sutra-select");
|
||||
const voiceSelect = document.getElementById("voice-select");
|
||||
const btnPlay = document.getElementById("btn-play");
|
||||
const statusEl = document.getElementById("status");
|
||||
@@ -105,6 +310,10 @@ const btnNext = document.getElementById("btn-next");
|
||||
const btnLoop = document.getElementById("btn-loop");
|
||||
const btnBgm = document.getElementById("btn-bgm");
|
||||
const timerSelect = document.getElementById("timer-select");
|
||||
const rateSelect = document.getElementById("rate-select");
|
||||
const btnNight = document.getElementById("btn-night");
|
||||
const sleepRemainEl = document.getElementById("sleep-remaining");
|
||||
const btnJumpNow = document.getElementById("btn-jump-now");
|
||||
const pageInfo = document.getElementById("page-info");
|
||||
|
||||
/** 音频缓存:key = `${经id}|${音色}|${段序号}`,value = Blob */
|
||||
@@ -117,8 +326,27 @@ let bgmOn = false; // 背景音乐开关
|
||||
let sleepEndAt = 0; // 定时停止的截止时间戳(毫秒),0 = 未开启
|
||||
let sleepTimer = null; // 定时器句柄(前台兜底;后台由 onTimeUpdate 判定截止时间)
|
||||
let loopAutoByTimer = false; // 循环是否为设定定时时自动打开的(是则定时结束一并关回)
|
||||
|
||||
// ===== 用户偏好记忆(localStorage):经、音色、字号、循环、配乐、语速、夜间 =====
|
||||
const PREFS_KEY = "fj.prefs.v1";
|
||||
const prefs = (() => {
|
||||
try {
|
||||
return JSON.parse(localStorage.getItem(PREFS_KEY)) || {};
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
})();
|
||||
/** 写一条偏好并落盘;localStorage 不可用时静默降级为仅内存。 */
|
||||
function savePref(key, value) {
|
||||
prefs[key] = value;
|
||||
try {
|
||||
localStorage.setItem(PREFS_KEY, JSON.stringify(prefs));
|
||||
} catch { /* 隐私模式等场景忽略 */ }
|
||||
}
|
||||
let segTimes = []; // 合并音频中各段的起始秒
|
||||
let currentSeg = -1; // 正在朗读的段号
|
||||
let lastUserScroll = 0; // 用户最近一次手动滚动经文的时间戳(用于暂缓跟读强拽)
|
||||
let cursorRaf = null; // 逐字光标 rAF 句柄
|
||||
|
||||
// ===== 背景音乐:Web Audio 实时合成颂钵泛音 drone(零资源文件、无缝循环) =====
|
||||
let bgmCtx = null;
|
||||
@@ -411,17 +639,61 @@ VOICE_GROUPS.forEach((group) => {
|
||||
});
|
||||
voiceSelect.value = "Seren";
|
||||
|
||||
// ===== 状态 / 高亮 =====
|
||||
function setStatus(text, isError = false) {
|
||||
statusEl.textContent = text;
|
||||
statusEl.classList.toggle("error", isError);
|
||||
// ===== 经典下拉:按 cat 分组(佛经 / 道家) =====
|
||||
{
|
||||
const cats = [...new Set(SUTRAS.map((s) => s.cat || "其他"))];
|
||||
cats.forEach((cat) => {
|
||||
const group = sutraSelect.appendChild(Object.assign(document.createElement("optgroup"), { label: cat }));
|
||||
SUTRAS.filter((s) => (s.cat || "其他") === cat).forEach((s) => {
|
||||
const opt = document.createElement("option");
|
||||
opt.value = s.id;
|
||||
opt.textContent = s.name;
|
||||
group.appendChild(opt);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ===== 状态 / 高亮 =====
|
||||
let statusBase = ""; // 状态主干文字;定时开启时自动追加剩余倒计时
|
||||
function setStatus(text, isError = false) {
|
||||
statusBase = text;
|
||||
statusEl.classList.toggle("error", isError);
|
||||
renderStatus();
|
||||
}
|
||||
|
||||
/** 渲染状态条:主干 + 定时剩余 mm:ss;移动端同步到播放旁的剩余胶囊。 */
|
||||
function renderStatus() {
|
||||
let remain = "";
|
||||
if (sleepEndAt) {
|
||||
const s = Math.max(0, Math.ceil((sleepEndAt - Date.now()) / 1000));
|
||||
remain = `${Math.floor(s / 60)}:${String(s % 60).padStart(2, "0")}`;
|
||||
}
|
||||
statusEl.textContent = statusBase + (remain ? ` · ${remain} 后停` : "");
|
||||
sleepRemainEl.textContent = remain ? `⏱ ${remain}` : "";
|
||||
sleepRemainEl.classList.toggle("show", !!sleepEndAt);
|
||||
}
|
||||
setInterval(renderStatus, 1000);
|
||||
|
||||
function setPlayingUI(on) {
|
||||
playing = on;
|
||||
btnPlay.classList.toggle("playing", on);
|
||||
btnPlay.setAttribute("aria-label", on ? "停止" : "播放");
|
||||
btnPlay.querySelector(".play-label").textContent = on ? "停 止" : "播 放";
|
||||
if ("mediaSession" in navigator) navigator.mediaSession.playbackState = on ? "playing" : "none";
|
||||
}
|
||||
|
||||
/** 更新锁屏/控制中心元数据:经名 + 音色 + 印章封面,并接管播放控制按键。 */
|
||||
function updateMediaSession() {
|
||||
if (!("mediaSession" in navigator) || !sutra) return;
|
||||
navigator.mediaSession.metadata = new MediaMetadata({
|
||||
title: `《${sutra.name}》诵读`,
|
||||
artist: currentVoice(),
|
||||
album: sutra.title,
|
||||
artwork: [
|
||||
{ src: "icon-180.png", sizes: "180x180", type: "image/png" },
|
||||
{ src: "icon.svg", sizes: "any", type: "image/svg+xml" },
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
function highlight(index) {
|
||||
@@ -434,22 +706,57 @@ function highlight(index) {
|
||||
d.classList.toggle("now", i === index);
|
||||
d.classList.toggle("done", index >= 0 && i < index);
|
||||
});
|
||||
// 跟读滚动:把正在朗读的段落平滑移到显示区中央
|
||||
// 跟读滚动:定位到光标所在行(而非整段中心);
|
||||
// 用户刚手动滚动过(6 秒内)则不打断阅读,改由"回到诵读处"按钮兜底
|
||||
const active = segEls.find((el) => Number(el.dataset.index) === index);
|
||||
active?.scrollIntoView({ behavior: "smooth", block: "center", inline: "center" });
|
||||
const userBusy = playing && Date.now() - lastUserScroll < 6000;
|
||||
if (active && !userBusy) {
|
||||
const target = active.querySelector(".ch.cur") || active;
|
||||
target.scrollIntoView({ behavior: reduceMotionMQ.matches ? "auto" : "smooth", block: "center", inline: "center" });
|
||||
}
|
||||
requestAnimationFrame(updateJumpNow);
|
||||
}
|
||||
|
||||
/** 诵读光标是否在经文视区内;不在则显示"回到诵读处"浮动按钮。 */
|
||||
function updateJumpNow() {
|
||||
if (!playing || currentSeg < 0) {
|
||||
btnJumpNow.hidden = true;
|
||||
return;
|
||||
}
|
||||
const active = segEls.find((el) => Number(el.dataset.index) === currentSeg);
|
||||
if (!active) {
|
||||
btnJumpNow.hidden = false; // 当前段不在本页,提示可跳回
|
||||
return;
|
||||
}
|
||||
const target = active.querySelector(".ch.cur") || active;
|
||||
const r = target.getBoundingClientRect();
|
||||
const c = sutraEl.getBoundingClientRect();
|
||||
btnJumpNow.hidden = r.top >= c.top && r.bottom <= c.bottom;
|
||||
}
|
||||
|
||||
function clearHighlight() {
|
||||
segEls.forEach((el) => el.classList.remove("playing", "done"));
|
||||
segEls.forEach((el) => {
|
||||
el.classList.remove("playing", "done");
|
||||
el.querySelectorAll(".ch").forEach((s) => s.classList.remove("read", "cur"));
|
||||
delete el.dataset.cursorN;
|
||||
});
|
||||
dots.forEach((d) => d.classList.remove("now", "done"));
|
||||
if (cursorRaf) {
|
||||
cancelAnimationFrame(cursorRaf);
|
||||
cursorRaf = null;
|
||||
}
|
||||
}
|
||||
|
||||
const reduceMotionMQ = matchMedia("(prefers-reduced-motion: reduce)");
|
||||
|
||||
// ===== 渲染 =====
|
||||
function renderDots() {
|
||||
progressEl.innerHTML = "";
|
||||
dots = segments.map(() => {
|
||||
dots = segments.map((_, i) => {
|
||||
const d = document.createElement("span");
|
||||
d.className = "dot";
|
||||
d.title = `第 ${i + 1} 段,点击起读`;
|
||||
d.addEventListener("click", () => playFrom(i));
|
||||
progressEl.appendChild(d);
|
||||
return d;
|
||||
});
|
||||
@@ -527,6 +834,7 @@ function selectSutra(id) {
|
||||
if (!next || next === sutra) return;
|
||||
stopPlayback();
|
||||
sutra = next;
|
||||
savePref("sutra", id);
|
||||
segments = sutra.segments;
|
||||
pageCount = Math.ceil(segments.length / sutra.pageSize);
|
||||
|
||||
@@ -540,9 +848,8 @@ function selectSutra(id) {
|
||||
src.textContent = sutra.source;
|
||||
src.href = sutra.sourceUrl;
|
||||
|
||||
document.querySelectorAll(".sutra-btn").forEach((b) => {
|
||||
b.classList.toggle("active", b.dataset.sutra === id);
|
||||
});
|
||||
sutraSelect.value = id;
|
||||
sheetSyncFns.get(sutraSelect)?.();
|
||||
|
||||
renderDots();
|
||||
renderPage(0);
|
||||
@@ -554,6 +861,7 @@ function selectSutra(id) {
|
||||
function stopPlayback() {
|
||||
playToken += 1;
|
||||
setPlayingUI(false);
|
||||
btnPlay.classList.remove("preparing");
|
||||
if (inflight) {
|
||||
inflight.abort();
|
||||
inflight = null;
|
||||
@@ -563,11 +871,14 @@ function stopPlayback() {
|
||||
audioEl.onended = null;
|
||||
stopBgm();
|
||||
clearHighlight();
|
||||
btnJumpNow.hidden = true;
|
||||
currentSeg = -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定段的音频 Blob,命中缓存则直接返回。
|
||||
* 失败自动重试(最多 3 次,间隔 1s/2s 递增),吸收服务重启等瞬时 502;
|
||||
* 用户停止播放导致的 AbortError 不重试。
|
||||
* @param {number} index 段序号
|
||||
* @returns {Promise<Blob>} 音频数据
|
||||
*/
|
||||
@@ -575,24 +886,32 @@ async function fetchSegment(index) {
|
||||
const key = `${sutra.id}|${currentVoice()}|${index}`;
|
||||
if (audioCache.has(key)) return audioCache.get(key);
|
||||
|
||||
inflight = new AbortController();
|
||||
try {
|
||||
const resp = await fetch("/api/tts", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ text: segments[index].t, voice: currentVoice() }),
|
||||
signal: inflight.signal,
|
||||
});
|
||||
if (!resp.ok) {
|
||||
const err = await resp.json().catch(() => ({}));
|
||||
throw new Error(err.error || `HTTP ${resp.status}`);
|
||||
let lastErr = null;
|
||||
for (let attempt = 0; attempt < 3; attempt += 1) {
|
||||
if (attempt > 0) await new Promise((r) => setTimeout(r, attempt * 1000));
|
||||
inflight = new AbortController();
|
||||
try {
|
||||
const resp = await fetch("/api/tts", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ text: segments[index].t, voice: currentVoice() }),
|
||||
signal: inflight.signal,
|
||||
});
|
||||
if (!resp.ok) {
|
||||
const err = await resp.json().catch(() => ({}));
|
||||
throw new Error(err.error || `HTTP ${resp.status}`);
|
||||
}
|
||||
const blob = await resp.blob();
|
||||
audioCache.set(key, blob);
|
||||
return blob;
|
||||
} catch (err) {
|
||||
if (err.name === "AbortError") throw err; // 主动停止,不重试
|
||||
lastErr = err;
|
||||
} finally {
|
||||
inflight = null;
|
||||
}
|
||||
const blob = await resp.blob();
|
||||
audioCache.set(key, blob);
|
||||
return blob;
|
||||
} finally {
|
||||
inflight = null;
|
||||
}
|
||||
throw lastErr;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -667,6 +986,65 @@ async function buildCombined(blobs) {
|
||||
return { url: URL.createObjectURL(new Blob([out], { type: "audio/wav" })), times };
|
||||
}
|
||||
|
||||
/** 把段落文字逐字包成 .ch span(仅当前诵读段需要,幂等)。 */
|
||||
function wrapChars(el) {
|
||||
if (el.dataset.wrapped) return;
|
||||
const frag = document.createDocumentFragment();
|
||||
for (const c of el.textContent) {
|
||||
const s = document.createElement("span");
|
||||
s.className = "ch";
|
||||
s.textContent = c;
|
||||
frag.appendChild(s);
|
||||
}
|
||||
el.innerHTML = "";
|
||||
el.appendChild(frag);
|
||||
el.dataset.wrapped = "1";
|
||||
}
|
||||
|
||||
/**
|
||||
* 逐字光标:按音频当前时间在当前段内按比例推进,
|
||||
* 已读字染朱砂、当前字挂金色下划线光标。TTS 无逐字时间戳,按语速均匀估算。
|
||||
*/
|
||||
function cursorTick() {
|
||||
if (!playing || !segTimes.length) {
|
||||
cursorRaf = null;
|
||||
return;
|
||||
}
|
||||
const t = audioEl.currentTime;
|
||||
let i = segTimes.length - 1;
|
||||
while (i > 0 && segTimes[i] > t) i -= 1;
|
||||
const active = segEls.find((el) => Number(el.dataset.index) === i);
|
||||
if (active) {
|
||||
wrapChars(active);
|
||||
const chs = active.querySelectorAll(".ch");
|
||||
const segDur = (i + 1 < segTimes.length ? segTimes[i + 1] - 0.8 : audioEl.duration - 3) - segTimes[i];
|
||||
const n = Math.min(chs.length, Math.floor(((t - segTimes[i]) / Math.max(segDur, 0.1)) * chs.length));
|
||||
const prev = Number(active.dataset.cursorN ?? -1);
|
||||
if (n !== prev) {
|
||||
const lo = Math.max(0, Math.min(n, prev));
|
||||
const hi = Math.max(n, prev);
|
||||
for (let k = lo; k <= hi && k < chs.length; k += 1) chs[k].classList.toggle("read", k < n);
|
||||
active.dataset.cursorN = n;
|
||||
const old = active.querySelector(".ch.cur");
|
||||
old?.classList.remove("cur");
|
||||
if (n < chs.length) chs[n].classList.add("cur");
|
||||
}
|
||||
cursorTick._n = (cursorTick._n || 0) + 1;
|
||||
if (cursorTick._n % 15 === 0) {
|
||||
// 光标行跟随:光标走出视区 25%~75% 舒适带时,平滑滚回中部
|
||||
if (Date.now() - lastUserScroll >= 6000 && chs[n]) {
|
||||
const r = chs[n].getBoundingClientRect();
|
||||
const c = sutraEl.getBoundingClientRect();
|
||||
if (r.top < c.top + c.height * 0.25 || r.bottom > c.top + c.height * 0.75) {
|
||||
chs[n].scrollIntoView({ behavior: reduceMotionMQ.matches ? "auto" : "smooth", block: "center", inline: "center" });
|
||||
}
|
||||
}
|
||||
updateJumpNow();
|
||||
}
|
||||
}
|
||||
cursorRaf = requestAnimationFrame(cursorTick);
|
||||
}
|
||||
|
||||
/** 播放进度回调:按当前时间定位段号,高亮并自动翻页滚动;顺带判定定时停止。 */
|
||||
function onTimeUpdate() {
|
||||
if (sleepEndAt && Date.now() >= sleepEndAt) {
|
||||
@@ -692,6 +1070,9 @@ async function playFrom(index) {
|
||||
stopPlayback();
|
||||
const token = ++playToken;
|
||||
setPlayingUI(true);
|
||||
// 准备阶段按钮脉冲 + 文字提示(移动端没有状态栏,这条反馈很关键)
|
||||
btnPlay.classList.add("preparing");
|
||||
btnPlay.querySelector(".play-label").textContent = "准备中";
|
||||
setStatus("正在准备音频…");
|
||||
|
||||
const blobs = new Array(segments.length);
|
||||
@@ -725,12 +1106,17 @@ async function playFrom(index) {
|
||||
};
|
||||
audioEl.currentTime = built.times[index];
|
||||
await audioEl.play();
|
||||
btnPlay.classList.remove("preparing");
|
||||
btnPlay.querySelector(".play-label").textContent = "停 止";
|
||||
if (bgmOn) startBgm(); // play() 成功后起步背景音乐(用户手势上下文内)
|
||||
updateMediaSession();
|
||||
cursorRaf = requestAnimationFrame(cursorTick); // 启动逐字光标
|
||||
setStatus(`正在朗读 · ${currentVoice()}`);
|
||||
} catch (err) {
|
||||
if (err.name === "AbortError") return; // 已被停止/新播放取代
|
||||
setStatus(`语音合成失败:${err.message}`, true);
|
||||
setPlayingUI(false);
|
||||
btnPlay.classList.remove("preparing");
|
||||
clearHighlight();
|
||||
}
|
||||
}
|
||||
@@ -747,6 +1133,7 @@ function setLoopMode(on) {
|
||||
btnLoop.addEventListener("click", () => {
|
||||
setLoopMode(!loopMode);
|
||||
loopAutoByTimer = false; // 用户手动干预后以手动状态为准
|
||||
savePref("loop", loopMode);
|
||||
setStatus(loopMode ? "循环诵读:开" : "循环诵读:关");
|
||||
});
|
||||
|
||||
@@ -755,6 +1142,7 @@ function fireSleepTimer() {
|
||||
sleepEndAt = 0;
|
||||
sleepTimer = null;
|
||||
timerSelect.value = "0";
|
||||
sheetSyncFns.get(timerSelect)?.();
|
||||
if (loopAutoByTimer) {
|
||||
loopAutoByTimer = false;
|
||||
setLoopMode(false);
|
||||
@@ -796,11 +1184,28 @@ btnBgm.addEventListener("click", () => {
|
||||
bgmOn = !bgmOn;
|
||||
btnBgm.classList.toggle("active", bgmOn);
|
||||
btnBgm.setAttribute("aria-pressed", String(bgmOn));
|
||||
savePref("bgm", bgmOn);
|
||||
if (bgmOn && playing) startBgm();
|
||||
if (!bgmOn) stopBgm();
|
||||
setStatus(bgmOn ? "背景音乐:开" : "背景音乐:关");
|
||||
});
|
||||
|
||||
/** 语速切换:直接改已合并音频的 playbackRate,立即生效。 */
|
||||
rateSelect.addEventListener("change", () => {
|
||||
audioEl.playbackRate = Number(rateSelect.value);
|
||||
savePref("rate", rateSelect.value);
|
||||
setStatus(`语速:${rateSelect.options[rateSelect.selectedIndex].text}`);
|
||||
});
|
||||
|
||||
/** 夜间模式:暗色宣纸配色,睡前诵读不刺眼。 */
|
||||
btnNight.addEventListener("click", () => {
|
||||
const on = document.body.classList.toggle("night");
|
||||
btnNight.classList.toggle("active", on);
|
||||
btnNight.setAttribute("aria-pressed", String(on));
|
||||
btnNight.textContent = on ? "日间" : "夜间";
|
||||
savePref("night", on);
|
||||
});
|
||||
|
||||
btnPlay.addEventListener("click", () => {
|
||||
if (playing || inflight) {
|
||||
stopPlayback();
|
||||
@@ -812,6 +1217,7 @@ btnPlay.addEventListener("click", () => {
|
||||
|
||||
voiceSelect.addEventListener("change", () => {
|
||||
if (playing || inflight) stopPlayback();
|
||||
savePref("voice", voiceSelect.value);
|
||||
setStatus(`已切换音色:${voiceSelect.value},点击播放开始诵读`);
|
||||
});
|
||||
|
||||
@@ -826,9 +1232,25 @@ btnExpand.addEventListener("click", () => {
|
||||
btnPrev.addEventListener("click", () => gotoPage(currentPage - 1));
|
||||
btnNext.addEventListener("click", () => gotoPage(currentPage + 1));
|
||||
|
||||
document.querySelectorAll(".sutra-btn").forEach((btn) => {
|
||||
btn.addEventListener("click", () => selectSutra(btn.dataset.sutra));
|
||||
// 手动滚轮/触摸滚动经文:记录时间,6 秒内跟读滚动让位给读者
|
||||
["wheel", "touchmove"].forEach((ev) => {
|
||||
sutraEl.addEventListener(ev, () => { lastUserScroll = Date.now(); }, { passive: true });
|
||||
});
|
||||
// 任何滚动(含自动滚动)都刷新"回到诵读处"按钮的可见性
|
||||
sutraEl.addEventListener("scroll", () => { if (playing) updateJumpNow(); }, { passive: true });
|
||||
btnJumpNow.addEventListener("click", () => {
|
||||
lastUserScroll = 0;
|
||||
let active = segEls.find((el) => Number(el.dataset.index) === currentSeg);
|
||||
if (!active && currentSeg >= 0) {
|
||||
renderPage(pageOf(currentSeg)); // 当前段不在本页,先翻过去
|
||||
active = segEls.find((el) => Number(el.dataset.index) === currentSeg);
|
||||
}
|
||||
const target = active?.querySelector(".ch.cur") || active;
|
||||
target?.scrollIntoView({ behavior: "smooth", block: "center", inline: "center" });
|
||||
btnJumpNow.hidden = true;
|
||||
});
|
||||
|
||||
sutraSelect.addEventListener("change", () => selectSutra(sutraSelect.value));
|
||||
|
||||
// 键盘 ← → 翻页(下拉框聚焦时不拦截)
|
||||
document.addEventListener("keydown", (e) => {
|
||||
@@ -838,12 +1260,17 @@ document.addEventListener("keydown", (e) => {
|
||||
});
|
||||
|
||||
// ===== 字号与排版 =====
|
||||
/** 应用字号档位并同步按钮高亮。 */
|
||||
function setFontSize(size) {
|
||||
if (!["sm", "md", "lg"].includes(size)) return;
|
||||
document.querySelectorAll(".size-btn").forEach((b) => b.classList.toggle("active", b.dataset.size === size));
|
||||
sutraEl.classList.remove("size-sm", "size-md", "size-lg");
|
||||
sutraEl.classList.add(`size-${size}`);
|
||||
}
|
||||
document.querySelectorAll(".size-btn").forEach((btn) => {
|
||||
btn.addEventListener("click", () => {
|
||||
document.querySelectorAll(".size-btn").forEach((b) => b.classList.remove("active"));
|
||||
btn.classList.add("active");
|
||||
sutraEl.classList.remove("size-sm", "size-md", "size-lg");
|
||||
sutraEl.classList.add(`size-${btn.dataset.size}`);
|
||||
setFontSize(btn.dataset.size);
|
||||
savePref("size", btn.dataset.size);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -862,8 +1289,133 @@ mobileMQ.addEventListener("change", () => {
|
||||
}
|
||||
});
|
||||
|
||||
// ===== 初始化(支持 #jingang / #vertical 锚点;手机端忽略竖排) =====
|
||||
// ===== 移动端底部升起选择器:原生 select 保留为状态源,手机上改由面板选择 =====
|
||||
const sheetSyncFns = new Map(); // select → 同步按钮文字的函数
|
||||
let sheetEls = null; // 当前打开的面板 {mask, panel}
|
||||
|
||||
/** 关闭底部选择面板(若开着)。 */
|
||||
function closeSheet() {
|
||||
if (!sheetEls) return;
|
||||
const { mask, panel } = sheetEls;
|
||||
sheetEls = null;
|
||||
mask.classList.remove("open");
|
||||
panel.classList.remove("open");
|
||||
setTimeout(() => { mask.remove(); panel.remove(); }, 220);
|
||||
}
|
||||
|
||||
/** 打开底部升起面板:列出 select 全部选项,点选后同步并派发 change。 */
|
||||
function openSheet(select, title) {
|
||||
closeSheet();
|
||||
const mask = document.createElement("div");
|
||||
mask.className = "sheet-mask";
|
||||
const panel = document.createElement("div");
|
||||
panel.className = "sheet-panel";
|
||||
panel.setAttribute("role", "dialog");
|
||||
panel.setAttribute("aria-label", title);
|
||||
panel.innerHTML = `<div class="sheet-grabber"></div><div class="sheet-title">${title}</div><div class="sheet-opts"></div>`;
|
||||
const opts = panel.querySelector(".sheet-opts");
|
||||
let lastGroup = null; // optgroup 分组标题(如"佛经/道家"),无分组时不渲染
|
||||
[...select.options].forEach((opt, i) => {
|
||||
const group = opt.parentElement?.tagName === "OPTGROUP" ? opt.parentElement.label : null;
|
||||
if (group && group !== lastGroup) {
|
||||
lastGroup = group;
|
||||
const gh = document.createElement("div");
|
||||
gh.className = "sheet-group";
|
||||
gh.textContent = group;
|
||||
opts.appendChild(gh);
|
||||
}
|
||||
const item = document.createElement("div");
|
||||
item.className = `sheet-opt${i === select.selectedIndex ? " sel" : ""}`;
|
||||
item.textContent = opt.textContent;
|
||||
item.setAttribute("role", "option");
|
||||
item.addEventListener("click", () => {
|
||||
select.value = opt.value;
|
||||
select.dispatchEvent(new Event("change"));
|
||||
closeSheet();
|
||||
});
|
||||
opts.appendChild(item);
|
||||
});
|
||||
const cancel = document.createElement("div");
|
||||
cancel.className = "sheet-cancel";
|
||||
cancel.textContent = "取 消";
|
||||
cancel.addEventListener("click", closeSheet);
|
||||
panel.appendChild(cancel);
|
||||
mask.addEventListener("click", closeSheet);
|
||||
document.body.appendChild(mask);
|
||||
document.body.appendChild(panel);
|
||||
sheetEls = { mask, panel };
|
||||
requestAnimationFrame(() => {
|
||||
mask.classList.add("open");
|
||||
panel.classList.add("open");
|
||||
});
|
||||
}
|
||||
|
||||
/** 为 select 生成伪下拉按钮(仅移动端显示),点击打开底部面板。 */
|
||||
function attachSheet(select, title) {
|
||||
if (!select) return;
|
||||
const btn = document.createElement("button");
|
||||
btn.type = "button";
|
||||
btn.className = "sheet-btn";
|
||||
btn.setAttribute("aria-haspopup", "dialog");
|
||||
const text = document.createElement("span");
|
||||
text.className = "sheet-btn-text";
|
||||
const sync = () => {
|
||||
text.textContent = select.options[select.selectedIndex]?.textContent || "";
|
||||
};
|
||||
sync();
|
||||
sheetSyncFns.set(select, sync);
|
||||
select.addEventListener("change", sync);
|
||||
btn.appendChild(text);
|
||||
select.insertAdjacentElement("afterend", btn);
|
||||
btn.addEventListener("click", () => openSheet(select, title));
|
||||
}
|
||||
attachSheet(sutraSelect, "选择经典");
|
||||
attachSheet(voiceSelect, "诵读音色");
|
||||
attachSheet(timerSelect, "定时停止");
|
||||
attachSheet(rateSelect, "诵读语速");
|
||||
|
||||
// 锁屏/控制中心按键:上一段/下一段、暂停、停止
|
||||
if ("mediaSession" in navigator) {
|
||||
navigator.mediaSession.setActionHandler("play", () => { if (!playing && !inflight) playFrom(Math.max(currentSeg, 0)); });
|
||||
navigator.mediaSession.setActionHandler("pause", () => stopPlayback());
|
||||
navigator.mediaSession.setActionHandler("stop", () => stopPlayback());
|
||||
navigator.mediaSession.setActionHandler("previoustrack", () => playFrom(Math.max(currentSeg - 1, 0)));
|
||||
navigator.mediaSession.setActionHandler("nexttrack", () => { if (currentSeg + 1 < segments.length) playFrom(currentSeg + 1); });
|
||||
}
|
||||
|
||||
// ===== PWA:注册 Service Worker,使"添加到主屏幕"后无地址栏独立运行 =====
|
||||
if ("serviceWorker" in navigator) {
|
||||
window.addEventListener("load", () => navigator.serviceWorker.register("/sw.js").catch(() => {}));
|
||||
}
|
||||
|
||||
// ===== 初始化:恢复偏好(hash 锚点优先于记忆的经典/排版) =====
|
||||
if (prefs.voice && [...voiceSelect.options].some((o) => o.value === prefs.voice)) voiceSelect.value = prefs.voice;
|
||||
sheetSyncFns.get(voiceSelect)?.();
|
||||
if (prefs.size) setFontSize(prefs.size);
|
||||
if (prefs.loop) setLoopMode(true);
|
||||
if (prefs.bgm) {
|
||||
bgmOn = true;
|
||||
btnBgm.classList.add("active");
|
||||
btnBgm.setAttribute("aria-pressed", "true");
|
||||
}
|
||||
if (prefs.rate && [...rateSelect.options].some((o) => o.value === String(prefs.rate))) {
|
||||
rateSelect.value = prefs.rate;
|
||||
audioEl.playbackRate = Number(prefs.rate);
|
||||
sheetSyncFns.get(rateSelect)?.();
|
||||
}
|
||||
if (prefs.night) {
|
||||
document.body.classList.add("night");
|
||||
btnNight.classList.add("active");
|
||||
btnNight.setAttribute("aria-pressed", "true");
|
||||
btnNight.textContent = "日间";
|
||||
}
|
||||
|
||||
if (location.hash.includes("vertical") && !mobileMQ.matches) {
|
||||
btnLayout.click();
|
||||
}
|
||||
selectSutra(location.hash.includes("jingang") ? "jingang" : "xinjing");
|
||||
// URL hash 可直达某部经(如 #jingang、#daodejing),优先于记忆的经典
|
||||
const hashId = location.hash.replace(/^#/, "");
|
||||
const initSutraId = SUTRAS.some((s) => s.id === hashId)
|
||||
? hashId
|
||||
: (SUTRAS.some((s) => s.id === prefs.sutra) ? prefs.sutra : "xinjing");
|
||||
selectSutra(initSutraId);
|
||||
|
||||
@@ -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 icon.svg icon-180.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)
|
||||
|
||||
SSH_OPTS="-o StrictHostKeyChecking=no -o ConnectTimeout=15"
|
||||
SSH_CMD="ssh $SSH_OPTS"
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 137 KiB |
+30
-10
@@ -3,16 +3,23 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="theme-color" content="#f6f1e6">
|
||||
<meta name="description" content="心经、金刚经、阿弥陀经、普门品、道德经、清静经、太上感应篇诵读——千问语音合成,晨钟暮鼓配乐,逐段跟读高亮。">
|
||||
<title>心经</title>
|
||||
<link rel="icon" type="image/svg+xml" href="icon.svg">
|
||||
<link rel="apple-touch-icon" href="icon-180.png">
|
||||
<link rel="stylesheet" href="style.css?v=4">
|
||||
<link rel="manifest" href="manifest.json">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<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=9">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="site-logo" aria-hidden="true">
|
||||
<img src="icon.svg" alt="" class="logo-mark">
|
||||
<span class="logo-text">佛经诵读</span>
|
||||
<span class="logo-text">经典诵读</span>
|
||||
</div>
|
||||
|
||||
<main class="page">
|
||||
@@ -32,10 +39,10 @@
|
||||
<svg viewBox="0 0 24 24" class="icon icon-stop"><rect x="6" y="6" width="12" height="12" rx="1.5"/></svg>
|
||||
<span class="play-label">播 放</span>
|
||||
</button>
|
||||
<div class="tool-group" role="group" aria-label="经典">
|
||||
<span class="tool-name">经典</span>
|
||||
<button type="button" data-sutra="xinjing" class="sutra-btn active">心经</button>
|
||||
<button type="button" data-sutra="jingang" class="sutra-btn">金刚经</button>
|
||||
<span id="sleep-remaining" class="sleep-remaining" aria-label="定时剩余时间"></span>
|
||||
<div class="field-pair">
|
||||
<label class="tool-name" for="sutra-select">经典</label>
|
||||
<select id="sutra-select" aria-label="选择经典"></select>
|
||||
</div>
|
||||
<div class="player-meta">
|
||||
<label class="voice-label" for="voice-select">诵读音色</label>
|
||||
@@ -48,20 +55,32 @@
|
||||
<button type="button" data-size="md" class="size-btn active">中</button>
|
||||
<button type="button" data-size="sm" class="size-btn">小</button>
|
||||
</div>
|
||||
<span class="flex-break flex-break-2" aria-hidden="true"></span>
|
||||
<div class="tool-group" role="group" aria-label="排版">
|
||||
<button type="button" id="btn-layout" class="layout-btn">切换竖排</button>
|
||||
</div>
|
||||
<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-bgm" class="layout-btn" aria-pressed="false">配乐</button>
|
||||
<label class="tool-name" for="timer-select">定时</label>
|
||||
<select id="timer-select" aria-label="定时停止">
|
||||
<button type="button" id="btn-night" class="layout-btn" aria-pressed="false">夜间</button>
|
||||
<div class="field-pair">
|
||||
<label class="tool-name" for="timer-select">定时</label>
|
||||
<select id="timer-select" aria-label="定时停止">
|
||||
<option value="0">关闭</option>
|
||||
<option value="30">30 分钟</option>
|
||||
<option value="60">60 分钟</option>
|
||||
<option value="90">90 分钟</option>
|
||||
<option value="120">120 分钟</option>
|
||||
</select>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field-pair">
|
||||
<label class="tool-name" for="rate-select">语速</label>
|
||||
<select id="rate-select" aria-label="诵读语速">
|
||||
<option value="0.8">慢 0.8×</option>
|
||||
<option value="1" selected>常速</option>
|
||||
<option value="1.25">快 1.25×</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div id="status" class="status" role="status"></div>
|
||||
<button type="button" id="btn-expand" class="expand-btn" aria-expanded="false" aria-label="展开设置" title="设置">
|
||||
@@ -74,6 +93,7 @@
|
||||
|
||||
<article class="sutra-paper">
|
||||
<div id="sutra" class="sutra size-md" role="document"></div>
|
||||
<button type="button" id="btn-jump-now" class="jump-now" hidden>⤵ 回到诵读处</button>
|
||||
</article>
|
||||
|
||||
<nav class="pager" id="pager" aria-label="分页" hidden>
|
||||
@@ -89,6 +109,6 @@
|
||||
</footer>
|
||||
|
||||
<audio id="audio" preload="auto"></audio>
|
||||
<script src="app.js?v=4"></script>
|
||||
<script src="app.js?v=9"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "经典诵读",
|
||||
"short_name": "经典诵读",
|
||||
"description": "心经、金刚经、阿弥陀经、普门品、道德经、清静经、太上感应篇诵读——千问语音合成,晨钟暮鼓配乐,逐段跟读高亮。",
|
||||
"start_url": "/",
|
||||
"scope": "/",
|
||||
"display": "standalone",
|
||||
"orientation": "portrait",
|
||||
"background_color": "#f6f1e6",
|
||||
"theme_color": "#f6f1e6",
|
||||
"lang": "zh-CN",
|
||||
"icons": [
|
||||
{ "src": "/icon-192.png", "sizes": "192x192", "type": "image/png", "purpose": "any" },
|
||||
{ "src": "/icon-512.png", "sizes": "512x512", "type": "image/png", "purpose": "any" },
|
||||
{ "src": "/icon-180.png", "sizes": "180x180", "type": "image/png", "purpose": "any" }
|
||||
]
|
||||
}
|
||||
@@ -88,8 +88,8 @@ class Handler(SimpleHTTPRequestHandler):
|
||||
if not text or not voice:
|
||||
self._send_json({"error": "缺少 text 或 voice 参数"}, status=400)
|
||||
return
|
||||
if len(text) > 600:
|
||||
self._send_json({"error": "文本超过 600 字上限"}, status=400)
|
||||
if len(text) > 800:
|
||||
self._send_json({"error": "文本超过 800 字上限"}, status=400)
|
||||
return
|
||||
if not API_KEY:
|
||||
self._send_json({"error": "服务端未配置 DASHSCOPE_API_KEY"}, status=500)
|
||||
@@ -103,7 +103,10 @@ class Handler(SimpleHTTPRequestHandler):
|
||||
else:
|
||||
audio = synthesize(text, voice)
|
||||
CACHE_DIR.mkdir(exist_ok=True)
|
||||
cache_file.write_bytes(audio)
|
||||
# 先写临时文件再原子重命名,避免中途崩溃留下半截 WAV 毒化缓存
|
||||
tmp_file = cache_file.with_suffix(".tmp")
|
||||
tmp_file.write_bytes(audio)
|
||||
tmp_file.replace(cache_file)
|
||||
except urllib.error.HTTPError as exc:
|
||||
detail = exc.read().decode("utf-8", errors="replace")[:300]
|
||||
self._send_json({"error": f"TTS 服务返回 {exc.code}:{detail}"}, status=502)
|
||||
|
||||
@@ -159,6 +159,13 @@ body {
|
||||
.btn-play.playing .icon-play { display: none; }
|
||||
.btn-play.playing .icon-stop { display: block; }
|
||||
|
||||
/* 音频准备中:按钮轻微呼吸脉冲,提示正在合成/合并 */
|
||||
.btn-play.preparing { cursor: wait; animation: preparing-pulse 1.3s ease-in-out infinite; }
|
||||
@keyframes preparing-pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.55; }
|
||||
}
|
||||
|
||||
.player-meta { display: flex; align-items: center; gap: 12px; }
|
||||
.voice-label { font-size: 0.85rem; color: var(--ink-soft); letter-spacing: 0.2em; white-space: nowrap; }
|
||||
|
||||
@@ -179,7 +186,9 @@ body {
|
||||
}
|
||||
#voice-select:focus-visible { outline: 3px solid var(--gold-soft); outline-offset: 1px; }
|
||||
|
||||
#timer-select {
|
||||
#timer-select,
|
||||
#rate-select,
|
||||
#sutra-select {
|
||||
appearance: none;
|
||||
font-family: inherit;
|
||||
font-size: 0.86rem;
|
||||
@@ -194,10 +203,12 @@ body {
|
||||
padding: 6px 26px 6px 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
#timer-select:focus-visible { outline: 3px solid var(--gold-soft); outline-offset: 1px; }
|
||||
#timer-select:focus-visible, #rate-select:focus-visible, #sutra-select:focus-visible { outline: 3px solid var(--gold-soft); outline-offset: 1px; }
|
||||
#sutra-select { color: var(--ink); font-size: 0.9rem; }
|
||||
|
||||
/* ===== 字号 / 排版工具(并入控制栏) ===== */
|
||||
.tool-group { display: flex; align-items: center; gap: 8px; }
|
||||
.field-pair { display: flex; align-items: center; gap: 8px; }
|
||||
.tool-name { font-size: 0.8rem; color: var(--ink-soft); letter-spacing: 0.2em; }
|
||||
|
||||
.size-btn, .layout-btn, .sutra-btn, .pager-btn {
|
||||
@@ -234,10 +245,14 @@ body {
|
||||
}
|
||||
.status.error { color: var(--cinnabar); }
|
||||
|
||||
/* 移动端强制换行占位 / 折叠按钮 / 文字标签(桌面端隐藏) */
|
||||
/* 移动端强制换行占位 / 折叠按钮 / 文字标签 / 底部选择器按钮(桌面端隐藏) */
|
||||
.flex-break { display: none; }
|
||||
.expand-btn { display: none; }
|
||||
.play-label, .expand-label { display: none; }
|
||||
.sheet-btn { display: none; }
|
||||
|
||||
/* 定时剩余时间胶囊(仅移动端定时激活时显示,桌面端看状态栏) */
|
||||
.sleep-remaining { display: none; }
|
||||
|
||||
.progress {
|
||||
display: flex;
|
||||
@@ -250,6 +265,7 @@ body {
|
||||
border-radius: 2px;
|
||||
background: rgba(94, 72, 45, 0.15);
|
||||
transition: background 0.3s ease;
|
||||
cursor: pointer; /* 点击跳到对应段开始诵读 */
|
||||
}
|
||||
.progress .dot.done { background: var(--gold); }
|
||||
.progress .dot.now { background: var(--cinnabar); }
|
||||
@@ -327,13 +343,27 @@ body {
|
||||
transition: background 0.25s ease, color 0.25s ease;
|
||||
}
|
||||
.seg:hover { background: rgba(176, 138, 79, 0.14); }
|
||||
/* 诵读中高亮:朱砂底纹 + 左侧竖条 + 下划线,长文中一眼定位 */
|
||||
.seg.playing {
|
||||
background: rgba(158, 43, 37, 0.10);
|
||||
background: rgba(158, 43, 37, 0.12);
|
||||
color: var(--cinnabar-deep);
|
||||
box-shadow: inset 0 -2px 0 var(--cinnabar);
|
||||
box-shadow:
|
||||
inset 3px 0 0 var(--cinnabar),
|
||||
inset 0 -1px 0 var(--cinnabar);
|
||||
padding-left: 8px;
|
||||
}
|
||||
.seg.done { color: var(--ink-soft); }
|
||||
|
||||
/* 逐字光标:当前段内未读字变淡、已读字回朱砂色、光标字挂金线下划 */
|
||||
.seg.playing .ch { color: rgba(92, 83, 71, 0.5); transition: color 0.15s ease; }
|
||||
.seg.playing .ch.read { color: var(--cinnabar-deep); }
|
||||
.seg.playing .ch.cur {
|
||||
color: var(--cinnabar);
|
||||
box-shadow: inset 0 -2px 0 var(--gold);
|
||||
}
|
||||
body.night .seg.playing .ch { color: rgba(176, 160, 132, 0.55); }
|
||||
body.night .seg.playing .ch.read { color: #f0c9c2; }
|
||||
|
||||
/* 竖排:从右往左逐列铺满整宽 */
|
||||
.sutra.vertical {
|
||||
writing-mode: vertical-rl;
|
||||
@@ -348,7 +378,29 @@ body {
|
||||
overflow-y: hidden;
|
||||
letter-spacing: 0.22em;
|
||||
}
|
||||
.sutra.vertical .seg.playing { box-shadow: inset 2px 0 0 var(--cinnabar); }
|
||||
.sutra.vertical .seg.playing { box-shadow: inset 0 3px 0 var(--cinnabar), inset 2px 0 0 var(--cinnabar); padding-left: 1px; }
|
||||
|
||||
/* "回到诵读处"浮动胶囊:当前段滚出视野时出现 */
|
||||
.jump-now {
|
||||
position: absolute;
|
||||
right: 16px;
|
||||
bottom: 16px;
|
||||
z-index: 10;
|
||||
font-family: inherit;
|
||||
font-size: 0.78rem;
|
||||
letter-spacing: 0.12em;
|
||||
color: var(--cinnabar);
|
||||
background: rgba(255, 252, 245, 0.95);
|
||||
border: 1px solid var(--gold);
|
||||
border-radius: 18px;
|
||||
padding: 8px 14px;
|
||||
cursor: pointer;
|
||||
box-shadow: var(--shadow);
|
||||
transition: transform 0.15s ease;
|
||||
}
|
||||
.jump-now:active { transform: scale(0.96); }
|
||||
.jump-now[hidden] { display: none; }
|
||||
body.night .jump-now { background: rgba(43, 36, 27, 0.92); }
|
||||
|
||||
/* 竖排:品名与正文恢复内联流式排布 */
|
||||
.sutra.vertical .chap {
|
||||
@@ -406,14 +458,14 @@ body {
|
||||
.subtitle { font-size: 0.78rem; letter-spacing: 0.3em; text-indent: 0.3em; margin-top: 4px; }
|
||||
|
||||
/* 控制栏:折叠为一行文字按钮(▶ 播放 长条 + 设置),点"设置"展开全部控件 */
|
||||
.player-card { padding: 8px 10px; }
|
||||
.player-row { gap: 8px; }
|
||||
.player-card { padding: 6px 8px; }
|
||||
.player-row { gap: 6px; }
|
||||
.btn-play {
|
||||
order: 1;
|
||||
flex: 1;
|
||||
width: auto;
|
||||
height: 38px;
|
||||
border-radius: 19px;
|
||||
height: 34px;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -425,12 +477,42 @@ body {
|
||||
.btn-play .play-label { display: inline; }
|
||||
.player-meta { flex: 1; min-width: 0; order: 2; }
|
||||
.flex-break { display: block; order: 3; flex-basis: 100%; height: 0; }
|
||||
.flex-break-2 { order: 6; } /* 展开态第二处换行:经典/字号 与 模式 分两行 */
|
||||
.tool-group { gap: 6px; }
|
||||
.tool-group[aria-label="经典"] { order: 4; }
|
||||
.tool-group[aria-label="字号"] { order: 5; }
|
||||
/* 经典下拉(field-pair 结构)与字号同行展开 */
|
||||
.player-row > .field-pair { order: 4; flex: 1; min-width: 0; }
|
||||
.player-row > .field-pair .sheet-btn, .player-row > .field-pair select { flex: 1; min-width: 0; }
|
||||
.tool-group[aria-label="字号"] { order: 5; flex: 1; justify-content: center; }
|
||||
.tool-group[aria-label="排版"] { order: 6; display: none; } /* 手机端不提供竖排 */
|
||||
.tool-group[aria-label="模式"] { order: 7; }
|
||||
/* 模式组:三个开关等宽一行,定时/语速两个下拉带小标签再占一行 */
|
||||
.tool-group[aria-label="模式"] { order: 7; flex: 1 1 100%; flex-wrap: wrap; }
|
||||
.tool-group[aria-label="模式"] .layout-btn { flex: 1 1 28%; text-align: center; }
|
||||
.tool-group[aria-label="模式"] .field-pair { flex: 1 1 46%; }
|
||||
.tool-group[aria-label="模式"] select { flex: 1; min-width: 0; }
|
||||
.tool-group[aria-label="模式"] .tool-name {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 0.68rem;
|
||||
color: var(--gold);
|
||||
letter-spacing: 0.06em;
|
||||
}
|
||||
.status { display: none; } /* 手机端去掉状态文字,只留经书内容 */
|
||||
/* 定时剩余胶囊:折叠/展开态均可见,夹在播放与设置之间 */
|
||||
.sleep-remaining.show {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
order: 2;
|
||||
flex: none;
|
||||
height: 34px;
|
||||
padding: 0 11px;
|
||||
font-size: 0.75rem;
|
||||
letter-spacing: 0.06em;
|
||||
color: var(--cinnabar);
|
||||
border: 1px solid var(--gold);
|
||||
border-radius: 6px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.player-meta { order: 2; } /* 展开态音色仍排第二位 */
|
||||
|
||||
/* 设置:与播放按钮同行的文字按钮 */
|
||||
.expand-btn {
|
||||
@@ -439,15 +521,15 @@ body {
|
||||
justify-content: center;
|
||||
order: 8;
|
||||
flex: none;
|
||||
height: 38px;
|
||||
padding: 0 16px;
|
||||
height: 34px;
|
||||
padding: 0 14px;
|
||||
font-family: inherit;
|
||||
font-size: 0.85rem;
|
||||
font-size: 0.8rem;
|
||||
letter-spacing: 0.12em;
|
||||
color: var(--ink-soft);
|
||||
background: transparent;
|
||||
border: 1px solid var(--hairline);
|
||||
border-radius: 19px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: color 0.15s ease, border-color 0.15s ease;
|
||||
}
|
||||
@@ -456,21 +538,24 @@ body {
|
||||
.expand-btn:hover { border-color: var(--gold); color: var(--ink); }
|
||||
.expand-btn:focus-visible { outline: 3px solid var(--gold-soft); outline-offset: 1px; }
|
||||
.expand-btn.open { color: var(--cinnabar); border-color: var(--cinnabar); }
|
||||
.player-card.expanded .expand-btn { order: 9; flex: 1 1 100%; } /* 展开后"收起"独占一行垫底 */
|
||||
|
||||
/* 折叠态:隐藏音色/经典/字号/排版控件与进度条,只留居中的播放按钮 */
|
||||
.player-card:not(.expanded) .player-meta,
|
||||
.player-card:not(.expanded) .tool-group,
|
||||
.player-card:not(.expanded) .field-pair,
|
||||
.player-card:not(.expanded) .progress,
|
||||
.player-card:not(.expanded) .flex-break { display: none; }
|
||||
.player-card:not(.expanded) .player-row { justify-content: center; }
|
||||
.player-card.expanded { padding-bottom: 12px; }
|
||||
.player-card.expanded { padding-bottom: 8px; }
|
||||
|
||||
.voice-label, .tool-name { display: none; }
|
||||
#voice-select { min-width: 0; width: 100%; }
|
||||
.voice-label { display: none; }
|
||||
.tool-name { display: none; } /* 默认隐藏小标签;模式组内定时/语速需要标签才可读 */
|
||||
#voice-select { min-width: 0; width: 100%; padding: 5px 30px 5px 10px; font-size: 0.82rem; }
|
||||
|
||||
/* 触屏目标 ≥36px,紧凑以容纳一行 */
|
||||
.size-btn { width: 36px; height: 36px; font-size: 0.85rem; }
|
||||
.layout-btn, .sutra-btn, .pager-btn { padding: 8px 10px; font-size: 0.82rem; min-height: 36px; }
|
||||
/* 触屏控件紧凑化 */
|
||||
.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::before { inset: 6px; }
|
||||
@@ -483,4 +568,148 @@ body {
|
||||
.pager { gap: 12px; }
|
||||
.page-info { min-width: 0; font-size: 0.8rem; }
|
||||
.site-footer { padding: 8px 14px; font-size: 0.7rem; }
|
||||
#timer-select, #rate-select { min-height: 30px; padding-top: 4px; padding-bottom: 4px; font-size: 0.78rem; }
|
||||
/* 原生下拉隐藏,改由底部升起面板选择 */
|
||||
#voice-select, #timer-select, #rate-select, #sutra-select { display: none; }
|
||||
.sheet-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 6px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
min-height: 30px;
|
||||
font-family: inherit;
|
||||
font-size: 0.78rem;
|
||||
letter-spacing: 0.08em;
|
||||
color: var(--ink-soft);
|
||||
background: transparent;
|
||||
border: 1px solid var(--hairline);
|
||||
border-radius: 6px;
|
||||
padding: 5px 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.sheet-btn::after {
|
||||
content: "▾";
|
||||
flex: none;
|
||||
font-size: 0.7rem;
|
||||
color: var(--gold);
|
||||
}
|
||||
.sheet-btn-text {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.progress { margin-top: 6px; }
|
||||
}
|
||||
|
||||
/* ===== 夜间模式:暗色宣纸底,整体降明度保留朱砂鎏金气质 ===== */
|
||||
body.night {
|
||||
--paper: #211c16;
|
||||
--paper-deep: #1a1611;
|
||||
--ink: #e8dcc2;
|
||||
--ink-soft: #b0a084;
|
||||
--cinnabar: #c96a5c;
|
||||
--cinnabar-deep: #a04a3e;
|
||||
--gold: #b9975f;
|
||||
--gold-soft: #5f4e30;
|
||||
--hairline: rgba(190, 158, 105, 0.28);
|
||||
--shadow: 0 10px 30px rgba(0, 0, 0, 0.45);
|
||||
background:
|
||||
radial-gradient(1200px 600px at 50% -8%, rgba(201, 106, 92, 0.06), transparent 60%),
|
||||
radial-gradient(900px 500px at 85% 110%, rgba(185, 151, 95, 0.08), transparent 55%),
|
||||
repeating-linear-gradient(0deg, transparent 0 26px, rgba(190, 158, 105, 0.02) 26px 27px),
|
||||
#211c16;
|
||||
}
|
||||
body.night .panel { background: rgba(43, 36, 27, 0.85); }
|
||||
body.night .sutra-paper {
|
||||
background:
|
||||
radial-gradient(800px 400px at 50% 0%, rgba(185, 151, 95, 0.05), transparent 60%),
|
||||
#292219;
|
||||
}
|
||||
body.night .sutra-paper::before { border-color: rgba(185, 151, 95, 0.22); }
|
||||
body.night .sutra-paper::after { border-color: rgba(185, 151, 95, 0.11); }
|
||||
body.night #voice-select { background-color: rgba(43, 36, 27, 0.9); color: var(--ink); }
|
||||
body.night #timer-select, body.night #rate-select, body.night #sutra-select { background-color: rgba(43, 36, 27, 0.6); }
|
||||
body.night .progress .dot { background: rgba(190, 158, 105, 0.2); }
|
||||
body.night .seg:hover { background: rgba(185, 151, 95, 0.12); }
|
||||
body.night .seg.playing { background: rgba(201, 106, 92, 0.16); color: #f0c9c2; }
|
||||
|
||||
/* ===== 底部升起选择面板(移动端) ===== */
|
||||
.sheet-mask {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 80;
|
||||
background: rgba(26, 20, 12, 0.45);
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
.sheet-mask.open { opacity: 1; }
|
||||
.sheet-panel {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 81;
|
||||
background: var(--paper);
|
||||
border-radius: 16px 16px 0 0;
|
||||
box-shadow: 0 -8px 30px rgba(0, 0, 0, 0.18);
|
||||
padding: 8px 0 max(14px, env(safe-area-inset-bottom));
|
||||
transform: translateY(105%);
|
||||
transition: transform 0.26s cubic-bezier(0.2, 0.8, 0.3, 1);
|
||||
max-height: 62vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.sheet-panel.open { transform: translateY(0); }
|
||||
.sheet-grabber {
|
||||
width: 36px;
|
||||
height: 4px;
|
||||
border-radius: 2px;
|
||||
background: var(--hairline);
|
||||
margin: 2px auto 8px;
|
||||
flex: none;
|
||||
}
|
||||
.sheet-title {
|
||||
text-align: center;
|
||||
font-size: 0.78rem;
|
||||
color: var(--ink-soft);
|
||||
letter-spacing: 0.24em;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid var(--hairline);
|
||||
flex: none;
|
||||
}
|
||||
.sheet-opts { overflow-y: auto; }
|
||||
.sheet-opt {
|
||||
padding: 13px 16px;
|
||||
text-align: center;
|
||||
font-size: 1rem;
|
||||
letter-spacing: 0.12em;
|
||||
color: var(--ink);
|
||||
border-bottom: 1px solid var(--hairline);
|
||||
cursor: pointer;
|
||||
}
|
||||
.sheet-opt:last-child { border-bottom: none; }
|
||||
/* 面板内的 optgroup 分组标题(佛经 / 道家) */
|
||||
.sheet-group {
|
||||
padding: 10px 16px 4px;
|
||||
text-align: center;
|
||||
font-size: 0.72rem;
|
||||
letter-spacing: 0.3em;
|
||||
color: var(--gold);
|
||||
}
|
||||
.sheet-opt.sel { color: var(--cinnabar); font-weight: 700; }
|
||||
.sheet-opt:active { background: rgba(176, 138, 79, 0.12); }
|
||||
.sheet-cancel {
|
||||
margin: 8px 16px 0;
|
||||
padding: 11px;
|
||||
text-align: center;
|
||||
font-size: 0.85rem;
|
||||
letter-spacing: 0.2em;
|
||||
color: var(--ink-soft);
|
||||
border: 1px solid var(--hairline);
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
flex: none;
|
||||
}
|
||||
.sheet-cancel:active { background: rgba(176, 138, 79, 0.12); }
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Service Worker:仅做最小化的安装支持与静态壳缓存。
|
||||
* - 静态资源(HTML/CSS/JS/图标)走"网络优先、失败回缓存",保证新版本及时生效
|
||||
* - /api/tts 不拦截,直接透传网络
|
||||
*/
|
||||
const CACHE = "fj-shell-v1";
|
||||
const SHELL = ["/", "/index.html", "/style.css?v=9", "/app.js?v=9", "/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()));
|
||||
});
|
||||
|
||||
self.addEventListener("activate", (e) => {
|
||||
e.waitUntil(
|
||||
caches.keys().then((keys) => Promise.all(keys.filter((k) => k !== CACHE).map((k) => caches.delete(k)))).then(() => self.clients.claim())
|
||||
);
|
||||
});
|
||||
|
||||
self.addEventListener("fetch", (e) => {
|
||||
const url = new URL(e.request.url);
|
||||
if (e.request.method !== "GET" || url.pathname.startsWith("/api/")) return;
|
||||
e.respondWith(
|
||||
fetch(e.request)
|
||||
.then((resp) => {
|
||||
if (resp.ok) {
|
||||
const clone = resp.clone();
|
||||
caches.open(CACHE).then((c) => c.put(e.request, clone));
|
||||
}
|
||||
return resp;
|
||||
})
|
||||
.catch(() => caches.match(e.request))
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user