Mayx's Home Page
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

350 lines
14 KiB

  1. /**
  2. * PJAX 初始化与页面切换重绑定脚本
  3. * 依赖jQuery, jquery.pjax.min.js
  4. * 加载顺序 jquery.pjax.min.js 之后body 末尾
  5. */
  6. (function ($) {
  7. // ========== 常量 ==========
  8. var CONTAINER = '#pjax-container';
  9. var PJAX_OPTS = {
  10. container: CONTAINER,
  11. fragment: CONTAINER,
  12. timeout: 8000,
  13. scrollTo: false
  14. };
  15. // ========== 工具函数 ==========
  16. var _loadedScripts = {};
  17. var _pendingScripts = [];
  18. /** 动态加载外部 CSS(避免重复加载) */
  19. function loadCSS(href) {
  20. if ($('link[href="' + href + '"]').length) return;
  21. $('<link rel="stylesheet" href="' + href + '" />').appendTo('head');
  22. }
  23. /**
  24. * 动态加载外部 JS避免重复
  25. * 用对象跟踪已加载的 URL而不是检查 DOM 中的 <script> 标签
  26. * pjax 替换容器内容后惰性 <script> 标签存在但不代表已执行
  27. */
  28. function loadScript(src, callback) {
  29. if (_loadedScripts[src]) {
  30. if (typeof callback === 'function') callback();
  31. return;
  32. }
  33. _loadedScripts[src] = true;
  34. var s = document.createElement('script');
  35. s.src = src;
  36. s.onload = callback || null;
  37. document.body.appendChild(s);
  38. }
  39. /**
  40. * 按顺序执行脚本数组内联和外部混合
  41. * 外部脚本加载完成后再执行后续内联脚本保持依赖顺序
  42. */
  43. function executeScripts(scripts) {
  44. var idx = 0;
  45. function runNext() {
  46. while (idx < scripts.length) {
  47. var s = scripts[idx];
  48. idx++;
  49. if (s.src) {
  50. loadScript(s.src, runNext);
  51. return; // 等待 onload 回调
  52. }
  53. try {
  54. (window.execScript || function (code) {
  55. window['eval'].call(window, code);
  56. })(s.text);
  57. } catch (e) {
  58. console.warn('[pjax] inline script exec error:', e);
  59. }
  60. }
  61. }
  62. runNext();
  63. }
  64. // ========== 页面类型判断 ==========
  65. /** 是否为文章页(非首页/分页) */
  66. function isPostPage(pathname) {
  67. return !/^(\/(index\.html)?|\/page\d+(\/index\.html)?)$/.test(pathname || window.location.pathname);
  68. }
  69. /** 是否为真正的文章页(用 DOM 特征判断,仅 post 布局才有这些元素) */
  70. function isRealPostPage() {
  71. return $(CONTAINER + ' #gitalk-container').length > 0;
  72. }
  73. // ========== 欢迎语生成 ==========
  74. /**
  75. * 根据当前时间和页面生成 Live2D 欢迎语
  76. * 此函数暴露到 window._live2d.getWelcomeText message.js 首次加载时复用
  77. * @param {string} [pathname] - 页面路径默认当前路径
  78. * @param {string} [title] - 页面标题默认从 document.title 提取
  79. * @returns {string} 欢迎语 HTML
  80. */
  81. function getWelcomeText(pathname, title) {
  82. pathname = pathname || window.location.pathname;
  83. title = title || document.title.split(' | ')[0];
  84. if (pathname === '/' || pathname === '/index.html') {
  85. var now = (new Date()).getHours();
  86. if (now > 23 || now <= 5) return '你是夜猫子呀?这么晚还不睡觉,明天起的来嘛?';
  87. if (now > 5 && now <= 7) return '早上好!一日之计在于晨,美好的一天就要开始了!';
  88. if (now > 7 && now <= 11) return '上午好!工作顺利嘛,不要久坐,多起来走动走动哦!';
  89. if (now > 11 && now <= 14) return '中午了,工作了一个上午,现在是午餐时间!';
  90. if (now > 14 && now <= 17) return '午后很容易犯困呢,今天的运动目标完成了吗?';
  91. if (now > 17 && now <= 19) return '傍晚了!窗外夕阳的景色很美丽呢,最美不过夕阳红~~';
  92. if (now > 19 && now <= 21) return '晚上好,今天过得怎么样?';
  93. if (now > 21 && now <= 23) return '已经这么晚了呀,早点休息吧,晚安~~';
  94. return '嗨~ 快来逗我玩吧!';
  95. }
  96. return '欢迎阅读<span style="color:#0099cc;">「 ' + title + ' 」</span>';
  97. }
  98. // ========== 各组件重初始化 ==========
  99. /** 访问量统计 */
  100. function reinitVisitors() {
  101. if (typeof BlogAPI === 'undefined') return;
  102. var apiBase = BlogAPI;
  103. if ($('.visitors').length === 1) {
  104. var $visitor = $('.visitors:first');
  105. $.get(apiBase + '/count_click_add?id=' + $visitor.attr('id'), function (data) {
  106. $visitor.text(Number(data));
  107. });
  108. } else if ($('.visitors-index').length > 0) {
  109. $('.visitors-index').each(function () {
  110. var $elem = $(this);
  111. $.get(apiBase + '/count_click?id=' + $elem.attr('id'), function (data) {
  112. $elem.text(Number(data));
  113. });
  114. });
  115. }
  116. }
  117. /** AI 摘要(post.html 内联脚本,pjax 后由 executeScripts 触发) */
  118. function reinitAISummary() {
  119. if (typeof ai_gen === 'function' && $('#ai-output').length) {
  120. try { ai_gen(); } catch (e) { /* ignore */ }
  121. }
  122. }
  123. /** 代码块复制按钮 */
  124. function reinitCopyButtons() {
  125. $('.copy').remove();
  126. $('div.highlight').each(function () {
  127. var $block = $(this);
  128. var $btn = $('<button>', { class: 'copy', type: 'button', text: '📋' });
  129. $block.append($btn);
  130. $btn.on('click', function () {
  131. var code = $btn.siblings('pre').find('code').text().trim();
  132. navigator.clipboard.writeText(code)
  133. .then(function () { $btn.text('✅'); })
  134. .catch(function () { $btn.text('❌'); })
  135. .finally(function () { setTimeout(function () { $btn.text('📋'); }, 1500); });
  136. });
  137. });
  138. }
  139. /** Gitalk 评论(post 页面专属) */
  140. function reinitGitalk() {
  141. if ($(CONTAINER + ' #gitalk-container').length === 0) return;
  142. loadCSS('/assets/css/gitalk.css');
  143. function doInitGitalk() {
  144. if (typeof Gitalk === 'undefined') {
  145. loadScript('/assets/js/gitalk.min.js', doInitGitalk);
  146. return;
  147. }
  148. var pageId = $(CONTAINER + ' #gitalk-container').data('page-id') || window.location.pathname;
  149. try {
  150. new Gitalk(Object.assign({ id: pageId }, window.GitalkConfig))
  151. .render('gitalk-container');
  152. } catch (e) {
  153. console.warn('[pjax] Gitalk init error:', e);
  154. }
  155. }
  156. $('#gitalk-container').empty();
  157. doInitGitalk();
  158. }
  159. /** 关键词高亮 */
  160. function reinitHighlight() {
  161. var keyword = new URLSearchParams(window.location.search).get('kw');
  162. if (!keyword) return;
  163. keyword = keyword.trim();
  164. if (!keyword) return;
  165. var escaped = keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
  166. var regex = new RegExp('(' + escaped + ')', 'gi');
  167. var escapeHTML = function (str) {
  168. return str.replace(/[&<>"']/g, function (t) {
  169. return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[t] || t;
  170. });
  171. };
  172. function walk(node) {
  173. $(node).contents().each(function () {
  174. if (this.nodeType === Node.TEXT_NODE) {
  175. var $t = $(this);
  176. var text = escapeHTML($t.text());
  177. if (regex.test(text)) $t.replaceWith(text.replace(regex, '<mark>$1</mark>'));
  178. } else if (this.nodeType === Node.ELEMENT_NODE && !$(this).is('script, style, noscript, textarea')) {
  179. walk(this);
  180. }
  181. });
  182. }
  183. $('section').each(function () { walk(this); });
  184. }
  185. /** Google Analytics 页面浏览事件 */
  186. function trackPageView() {
  187. if (typeof gtag === 'function') {
  188. gtag('config', window._gaId || '', { page_path: window.location.pathname });
  189. }
  190. }
  191. /** Live2D 重初始化 */
  192. var _live2dSelectors = ['.post-link', '#search-input'];
  193. var _live2dDelegateBound = false;
  194. function reinitLive2d() {
  195. if (!window._live2d) return;
  196. var pathname = window.location.pathname;
  197. // 更新"想问这篇文章"相关状态(仅真正的文章页显示)
  198. $('#post_id').val(pathname);
  199. if (isRealPostPage()) {
  200. $('.live_talk_input_name_body').show();
  201. } else {
  202. $('.live_talk_input_name_body').hide();
  203. $('#load_this').prop('checked', false);
  204. }
  205. // 音乐按钮:根据当前页面是否有 BGM 输入来显示/隐藏
  206. if (typeof window._live2d.initBGM === 'function') {
  207. window._live2d.initBGM();
  208. }
  209. // 事件委托绑定(只执行一次)
  210. if (!_live2dDelegateBound && typeof String.prototype.renderTip === 'function') {
  211. var selector = CONTAINER + ' ' + _live2dSelectors.join(', ' + CONTAINER + ' ');
  212. $(document).on('mouseover._live2d_pjax', selector, function (e) {
  213. var $el = $(e.currentTarget || e.target);
  214. if ($el.is('.post-link')) {
  215. window._live2d.showMessage('要看看 ' + $el.text() + ' 么?', 3000);
  216. } else if ($el.is('#search-input')) {
  217. window._live2d.showMessage('在找什么东西呢,需要帮忙吗?', 3000);
  218. }
  219. });
  220. $(document).on('mouseout._live2d_pjax', selector, function () {
  221. if (window._live2d.showHitokoto) window._live2d.showHitokoto();
  222. });
  223. _live2dDelegateBound = true;
  224. }
  225. // 欢迎语
  226. if (typeof window._live2d.showMessage === 'function') {
  227. window._live2d.showMessage(getWelcomeText(pathname), 6000);
  228. }
  229. }
  230. // ========== PJAX 导航 ==========
  231. /** PJAX 完成后的统一处理 */
  232. function doPjaxComplete() {
  233. $('body').removeClass('pjax-loading');
  234. // 清理可能残留的浮层(如推荐文章 tooltip,hover 后点击跳转时 mouseleave 来不及触发)
  235. $('.content-tooltip').hide();
  236. // go() 路径:脚本在 DOM 替换前提取到了 _pendingScripts,需在此执行
  237. // pjax 库路径:_pendingScripts 为空,pjax 库自行处理了脚本执行
  238. if (_pendingScripts.length > 0) {
  239. executeScripts(_pendingScripts);
  240. _pendingScripts = [];
  241. }
  242. onPjaxComplete();
  243. }
  244. /** 暴露给模板内 onclick/onchange 调用的导航函数 */
  245. window.go = function (url) {
  246. $('body').addClass('pjax-loading');
  247. $.ajax({
  248. url: url,
  249. beforeSend: function (xhr) {
  250. xhr.setRequestHeader('X-PJAX', 'true');
  251. xhr.setRequestHeader('X-PJAX-Container', CONTAINER);
  252. },
  253. success: function (html) {
  254. try {
  255. var doc = (new DOMParser()).parseFromString(html, 'text/html');
  256. var fragment = doc.querySelector(CONTAINER);
  257. if (fragment) {
  258. // 先提取脚本(jQuery html() 会移除并可能异步处理脚本)
  259. _pendingScripts = [];
  260. fragment.querySelectorAll('script').forEach(function (s) {
  261. _pendingScripts.push({
  262. src: s.src || null,
  263. text: s.textContent
  264. });
  265. s.remove();
  266. });
  267. $(CONTAINER).html(fragment.innerHTML);
  268. document.title = doc.title;
  269. history.pushState({ url: url }, document.title, url);
  270. doPjaxComplete();
  271. } else {
  272. window.location.href = url;
  273. }
  274. } catch (e) {
  275. console.warn('[go] parse error, fallback:', e);
  276. window.location.href = url;
  277. }
  278. },
  279. error: function () { window.location.href = url; },
  280. timeout: PJAX_OPTS.timeout
  281. });
  282. };
  283. /** 暴露 getWelcomeText 供 message.js 首次加载时复用,避免欢迎语逻辑重复 */
  284. window._pjaxGetWelcomeText = getWelcomeText;
  285. // ========== 初始化 ==========
  286. /** 每次 pjax 完成后执行所有重初始化 */
  287. function onPjaxComplete() {
  288. reinitVisitors();
  289. reinitCopyButtons();
  290. reinitHighlight();
  291. reinitGitalk();
  292. reinitAISummary();
  293. reinitLive2d();
  294. trackPageView();
  295. window.scrollTo(0, 0);
  296. }
  297. $(document).ready(function () {
  298. // 排除列表:外链、锚点、静态资源、Live2D 目录
  299. var exclude = ':not([target="_blank"]):not([href^="http"]):not([href^="//"])' +
  300. ':not([href^="mailto"]):not([href^="#"])' +
  301. ':not([href$=".xml"]):not([href$=".json"]):not([href$=".tgz"]):not([href$=".zip"])' +
  302. ':not([href^="/Live2dHistoire"])';
  303. $(document).pjax('a' + exclude, PJAX_OPTS.container, PJAX_OPTS);
  304. $(document).on('pjax:send', function () {
  305. $('body').addClass('pjax-loading');
  306. });
  307. $(document).on('pjax:complete', doPjaxComplete);
  308. $(document).on('pjax:error', function (xhr, textStatus, error) {
  309. console.warn('[pjax] error, fallback:', error);
  310. });
  311. // 首次加载初始化
  312. reinitCopyButtons();
  313. });
  314. })(jQuery);