🏠 Home 

「Z-Blog」论坛辅助

针对 Z-Blog 官方论坛的辅助脚本


ติดตั้งสคริปต์นี้?
สคริปต์ที่แนะนำของผู้เขียน

คุณอาจชื่นชอบ 「Z-Blog」开发者工具(应用中心)


ติดตั้งสคริปต์นี้
  1. // ==UserScript==
  2. // @name 「Z-Blog」论坛辅助
  3. // @namespace https://www.wdssmq.com/
  4. // @version 1.0.5
  5. // @author 沉冰浮水
  6. // @description 针对 Z-Blog 官方论坛的辅助脚本
  7. // @license MIT
  8. // @link https://greasyfork.org/zh-CN/scripts/419517
  9. // @null ----------------------------
  10. // @contributionURL https://github.com/wdssmq#%E4%BA%8C%E7%BB%B4%E7%A0%81
  11. // @contributionAmount 5.93
  12. // @null ----------------------------
  13. // @link https://github.com/wdssmq/userscript
  14. // @link https://afdian.net/@wdssmq
  15. // @link https://greasyfork.org/zh-CN/users/6865-wdssmq
  16. // @null ----------------------------
  17. // @noframes
  18. // @run-at document-end
  19. // @match https://bbs.zblogcn.com/*
  20. // @match https://app.zblogcn.com/zb_system/admin/edit.php*
  21. // @grant GM_xmlhttpRequest
  22. // @grant GM_setValue
  23. // @grant GM_getValue
  24. // @grant GM_addStyle
  25. // @require https://cdn.bootcdn.net/ajax/libs/lz-string/1.4.4/lz-string.min.js
  26. // @require https://cdn.bootcdn.net/ajax/libs/js-yaml/4.1.0/js-yaml.min.js
  27. // @require https://cdn.jsdelivr.net/npm/showdown@2.1.0/dist/showdown.min.js
  28. // ==/UserScript==
  29. /* eslint-disable */
  30. /* jshint esversion: 6 */
  31. (function () {
  32. 'use strict';
  33. const gm_name = "zbp-xiuno";
  34. // 初始变量
  35. const $n = (selector, context = document) => context.querySelector(selector);
  36. const $ = window.jQuery || unsafeWindow.jQuery;
  37. const UM = window.UM || unsafeWindow.UM;
  38. const UE = window.UE || unsafeWindow.UE;
  39. const curHref = location.href.replace(location.hash, "");
  40. // localStorage 封装
  41. const lsObj = {
  42. setItem: function(key, value) {
  43. localStorage.setItem(key, JSON.stringify(value));
  44. },
  45. getItem: function(key, def = "") {
  46. const item = localStorage.getItem(key);
  47. if (item) {
  48. return JSON.parse(item);
  49. }
  50. return def;
  51. },
  52. };
  53. // 预置函数
  54. const _log = (...args) => console.log(`[${gm_name}]\n`, ...args);
  55. const _hash = () => location.hash.replace("#", "");
  56. // Get 封装
  57. function fnGetRequest(strURL, strData, fnCallback) {
  58. if (typeof strData === "function") {
  59. fnCallback = strData;
  60. strData = "";
  61. }
  62. GM_xmlhttpRequest({
  63. method: "GET",
  64. data: strData,
  65. url: strURL,
  66. onload: function(responseDetail) {
  67. if (responseDetail.status === 200) {
  68. fnCallback(responseDetail.responseText, strURL);
  69. } else {
  70. console.log(responseDetail);
  71. alert("请求失败,请检查网络!");
  72. }
  73. },
  74. });
  75. }
  76. // formtTime 封装
  77. function fnFormatTime() {
  78. const objTime = new Date();
  79. const strYear = objTime.getFullYear();
  80. const strMonth = objTime.getMonth() + 1;
  81. const strDate = objTime.getDate();
  82. objTime.getHours();
  83. objTime.getMinutes();
  84. objTime.getSeconds();
  85. return (
  86. [strYear, strMonth, strDate].map(n => n.toString().padStart(2, "0")).join("-") +
  87. // " " +
  88. // [strHour, strMinute, strSecond].map((n) => n.toString().padStart(2, "0")).join(":") +
  89. ""
  90. ).trim();
  91. }
  92. (() => {
  93. const $body = $n("body");
  94. const defData = {
  95. status: 0, // 用于记录状态
  96. href: curHref,
  97. };
  98. // 更新 lsData 中的 status 状态
  99. const updateStatus = (status) => {
  100. const lsData = lsObj.getItem("xiuno_login", defData);
  101. lsData.status = status;
  102. lsObj.setItem("xiuno_login", lsData);
  103. };
  104. // 登录后跳转前登录前的页面
  105. const goUrl = () => {
  106. // 读取 localStorage
  107. const lsData = lsObj.getItem("xiuno_login", defData);
  108. // 根据记录的状态判断是否跳转
  109. if (lsData.status === 1) {
  110. // 更新状态
  111. updateStatus(2);
  112. // 跳转前的页面地址
  113. location.href = lsData.href;
  114. }
  115. };
  116. // 主入口函数,判断是否登录
  117. const checkLogin = () => {
  118. // 判断 body 内容是否为空
  119. if ($body.textContent.trim() !== "") {
  120. // 有内容,说明已登录,根据记录的地址跳转
  121. goUrl();
  122. return;
  123. }
  124. // 更新状态及 href 到 localStorage
  125. updateStatus(1);
  126. // 跳转登录页
  127. location.href = "/user-login.html";
  128. };
  129. // 延时 3 秒检查
  130. setTimeout(checkLogin, 3000);
  131. })();
  132. /* globals LZString jsyaml*/
  133. (() => {
  134. // 定义按钮及提示信息
  135. const $btnBad = $(" <a class=\"btn btn-primary\">BAD</a>");
  136. const strTip = "<p>此贴内容或签名不符合论坛规范已作屏蔽处理,请查看置顶贴,以下为原始内容备份。</p>";
  137. // 绑定点击事件
  138. $btnBad.css({ color: "#fff" }).click(function () {
  139. let um = UM.getEditor("message");
  140. let str = um.getContent();
  141. if (str.indexOf("#~~") > -1) {
  142. return;
  143. }
  144. let strCode = LZString.compressToBase64(str);
  145. um.setContent(strTip + `<p>#~~${strCode}~~#</p>`);
  146. console.log(LZString.decompressFromBase64(strCode));
  147. // let strDeCode = LZString.decompressFromBase64(strCode);
  148. // um.setContent(strCode + strDeCode);
  149. });
  150. // 放置按钮
  151. if ($("input[name=update_reason]").length > 0) {
  152. $("#submit").after($btnBad);
  153. }
  154. // 解码
  155. $("div.message").each(function () {
  156. let $secP = $(this).find("p:nth-child(2)");
  157. if ($secP.length == 0) {
  158. console.log("skip");
  159. return;
  160. }
  161. let str = $secP.html();
  162. if (str.indexOf("#~~") == -1) {
  163. return;
  164. }
  165. console.log(str);
  166. str = str.replace(/#~~(.+)~~#/, function (a, b) {
  167. console.log(arguments);
  168. let strDeCode = LZString.decompressFromBase64(b);
  169. console.log(strDeCode);
  170. return strDeCode;
  171. });
  172. $secP.after(str).remove();
  173. });
  174. })();
  175. // _pid.js | 楼层地址
  176. (() => {
  177. $("li.media.post").each(function () {
  178. const $me = $(this);
  179. const pid = $me.data("pid");
  180. const $date = $me.find("span.date");
  181. $date.after(
  182. `<a class="text-grey ml-2" title="获取当前楼层链接" href="${curHref}#${pid}">「楼层地址」</a>`,
  183. );
  184. });
  185. })();
  186. /* globals jsyaml*/
  187. (() => {
  188. // _log(curHref);
  189. if (curHref.indexOf("bbs.zblogcn.com") === -1) {
  190. return;
  191. }
  192. _log("devView");
  193. // CDN 地址替换
  194. function fnGetCDNUrl(url) {
  195. const arrMap = [
  196. ["https://github.com/", "https://cdn.jsdelivr.net/gh/"],
  197. ["/blob/", "@"],
  198. ];
  199. let cdnUrl = url;
  200. arrMap.forEach((line) => {
  201. cdnUrl = cdnUrl.replace(line[0], line[1]);
  202. });
  203. return cdnUrl;
  204. }
  205. // time 2 hour
  206. function fnTime2Hour(time = null) {
  207. if (!time) {
  208. time = new Date();
  209. }
  210. // 时间戳
  211. const timeStamp = time.getTime();
  212. return Math.floor(timeStamp / 1000 / 60 / 60);
  213. }
  214. // 默认配置项
  215. const defConfig = {
  216. useCDN: false,
  217. ymlList: [
  218. "2023H1",
  219. "2022H2",
  220. "2022H1",
  221. "2021H2",
  222. ],
  223. ver: "2023-04-24",
  224. isNew: true,
  225. };
  226. // 配置项读取和首次保存
  227. const curConfig = GM_getValue("_devConfig", defConfig);
  228. if (curConfig.isNew || curConfig.ver !== defConfig.ver) {
  229. curConfig.isNew = false;
  230. GM_setValue("_devConfig", defConfig);
  231. }
  232. // 初始化 ymlList
  233. function fnInitYML() {
  234. const useCDN = curConfig.useCDN;
  235. let ymlList = curConfig.ymlList;
  236. ymlList = ymlList.map((yml) => {
  237. let url = `https://raw.githubusercontent.com/wdssmq/ReviewLog/main/data/${yml}.yml`;
  238. if (useCDN) {
  239. url = fnGetCDNUrl(url);
  240. }
  241. return url;
  242. });
  243. return ymlList;
  244. }
  245. // 模板函数
  246. function fnStrtr(
  247. str,
  248. obj,
  249. callback = (str) => {
  250. return str;
  251. },
  252. ) {
  253. let rltStr = str;
  254. for (const key in obj) {
  255. if (Object.hasOwnProperty.call(obj, key)) {
  256. const value = obj[key];
  257. const reg = new RegExp(`#${key}#`, "g");
  258. rltStr = rltStr.replace(reg, value);
  259. }
  260. }
  261. return callback(rltStr);
  262. }
  263. // 数据读取封装
  264. const gobDev = {
  265. data: {
  266. lstLogs: [],
  267. lstCheck: null,
  268. },
  269. init: function () {
  270. this.data = lsObj.getItem("gobDev", this.data);
  271. _log("gobDev init", this.data);
  272. this.ymlList = fnInitYML();
  273. },
  274. checkUrl: function (url) {
  275. let rlt = null;
  276. this.data.lstLogs.forEach((log) => {
  277. if (log.url.indexOf(url) > -1) {
  278. _log("checkUrl", url, log.url);
  279. rlt = log;
  280. }
  281. });
  282. return rlt;
  283. },
  284. clear: function () {
  285. this.data.lstLogs = [];
  286. lsObj.setItem("gobDev", this.data);
  287. },
  288. save: function () {
  289. lsObj.setItem("gobDev", this.data);
  290. },
  291. update: function () {
  292. const curHour = fnTime2Hour();
  293. if (this.data.lstCheck === curHour && this.data.lstLogs.length > 0) {
  294. return;
  295. }
  296. this.data.lstLogs = [];
  297. this.data.lstCheck = curHour;
  298. this.ajax();
  299. },
  300. ajax: function () {
  301. const self = this;
  302. this.ymlList.forEach((yml) => {
  303. fnGetRequest(yml, (responseText, url) => {
  304. _log("ajax", url);
  305. const ymlObj = jsyaml.load(responseText, "utf8");
  306. const curLogs = self.data.lstLogs;
  307. self.data.lstLogs = curLogs.concat(ymlObj);
  308. self.save();
  309. });
  310. });
  311. },
  312. };
  313. gobDev.init();
  314. gobDev.update();
  315. // 缓存清理封装
  316. const _clearAct = (doClear = false) => {
  317. const curHash = _hash();
  318. if (curHash === "clearDone") {
  319. window.location.href = `${curHref}`;
  320. // window.location.reload();
  321. } else if (doClear || curHash === "clear") {
  322. gobDev.clear();
  323. window.location.href = `${curHref}#clearDone`;
  324. setTimeout(() => {
  325. window.location.reload();
  326. }, 1000);
  327. }
  328. };
  329. // 默认调用一次用于清后的跳转
  330. _clearAct();
  331. // 缓存清理按钮
  332. const $btnClear = $("<span class=\"small\"><a href=\"javascript:;\" title=\"清理缓存\" class=\"badge badge-warning\">清理缓存</a></span>");
  333. $btnClear.on("click", function () {
  334. if (confirm("清理缓存?")) {
  335. _clearAct(1);
  336. }
  337. });
  338. // 根据 log 数据设置状态徽章
  339. const _setBadge = (log, $item = null, act = "after") => {
  340. // console.log("log", log);
  341. let badgeClass, $badge;
  342. const status = log?.status || "未记录";
  343. switch (status) {
  344. case "通过":
  345. badgeClass = "badge-success";
  346. break;
  347. case "进行中":
  348. badgeClass = "badge-info";
  349. break;
  350. case "拒绝":
  351. badgeClass = "badge-danger";
  352. break;
  353. default:
  354. badgeClass = "badge-warning";
  355. break;
  356. }
  357. $badge = $(`<span class="badge ${badgeClass}">${status}</span>`);
  358. if (act === "after") {
  359. $item.after($badge);
  360. // $item.after($btnClear);
  361. } else {
  362. $item.append($badge);
  363. $item.append(" ");
  364. $item.append($btnClear);
  365. }
  366. };
  367. // 标题列表
  368. const $titleList = $("li.media .subject a");
  369. $titleList.each(function () {
  370. const $this = $(this);
  371. const href = $this.attr("href");
  372. const title = $this.text();
  373. if (title.indexOf("申请开发者") === -1) {
  374. return;
  375. }
  376. const log = gobDev.checkUrl(href);
  377. _setBadge(log, $this);
  378. });
  379. // 博文内页
  380. const $h4 = $(".media-body h4");
  381. let title = $h4.text().trim();
  382. if (title.indexOf("申请开发者") === -1) {
  383. return;
  384. }
  385. const log = gobDev.checkUrl(curHref);
  386. _setBadge(log, $h4, "append");
  387. _log("curLog", log);
  388. // 初始化
  389. $("div.message").each(function () {
  390. if ($(this).attr("isfirst") == 1) {
  391. $(this).prepend(
  392. "<blockquote class=\"blockquote\"><pre class=\"pre-yml\"></pre></blockquote>",
  393. );
  394. $(".pre-yml").text("标题格式错误");
  395. }
  396. });
  397. // 标题内容解析
  398. title = title.replace(/\[|【/g, "「").replace(/\]|】/g, "」");
  399. const objMatch = title.match(/「([^」]+)」「(theme|plugin)」/);
  400. _log("objMatch", objMatch);
  401. if (!objMatch) {
  402. return;
  403. }
  404. // YML 模板
  405. const tplYML = `
  406. - id: #id#
  407. type: #type#
  408. status: #status#
  409. rating: #rating#
  410. url: #url#
  411. git: #git#
  412. date:
  413. - #date#
  414. reviewers:
  415. - #reviewers#
  416. `;
  417. // 构建 YML
  418. const styYML = fnStrtr(
  419. tplYML,
  420. {
  421. id: objMatch[1],
  422. type: objMatch[2],
  423. status: log ? log.status : "进行中",
  424. rating: log ? log.rating : "",
  425. url: curHref,
  426. git: log ? log.git : "",
  427. date: log ? log.date[0] : fnFormatTime(),
  428. reviewers: log ? log.reviewers.join("\n_4_- ") : "null",
  429. },
  430. (str) => {
  431. str = str.replace(/\n/g, "\\|");
  432. // str = str.replace(/\s{6}/g, "_2__2_");
  433. // str = str.replace(/\s{4}/g, "_2_");
  434. str = str.replace(/_4_/g, "_2__2_");
  435. str = str.replace(/_2_/g, " ");
  436. str = str.replace(/\\\|/g, "\n");
  437. const objMatch = title.match(/(通过|拒绝)/);
  438. if (objMatch) {
  439. str = str.replace(/status: 进行中/, `status: ${objMatch[1]}`);
  440. }
  441. return str;
  442. },
  443. );
  444. // 插入 YML
  445. $(".pre-yml").text(`${styYML}`);
  446. })();
  447. // 引入元素插入
  448. (() => {
  449. if (typeof UM === "undefined") {
  450. return;
  451. }
  452. // 引用标签插入封装
  453. function fnBlockQuote() {
  454. const umObj = UM.getEditor("message");
  455. if (!umObj.isFocus()) {
  456. umObj.focus(true);
  457. }
  458. const addHTML = "<blockquote class=\"blockquote\"><p><br></p></blockquote><p><br></p>";
  459. // umObj.execCommand("insertHtml", addHTML);
  460. umObj.setContent(addHTML, true);
  461. }
  462. // 添加引用按钮
  463. $("head").append("<style>.edui-icon-blockquote:before{content:\"\\f10d\";}");
  464. (() => {
  465. const $btn = $.eduibutton({
  466. icon: "blockquote",
  467. click: function () {
  468. fnBlockQuote();
  469. },
  470. title: UM.getEditor("message").getLang("labelMap")["blockquote"] || "",
  471. });
  472. $(".edui-btn-name-insertcode").after($btn);
  473. })();
  474. // 自动排版函数封装
  475. function fnAutoFormat() {
  476. const umObj = UM.getEditor("message");
  477. let strHTML = umObj.getContent();
  478. strHTML = strHTML.replace(
  479. /<blockquote>/g,
  480. "<blockquote class=\"blockquote\">",
  481. );
  482. // 第二个参数为 true 表示追加;
  483. umObj.setContent(strHTML, false);
  484. }
  485. // 添加自动排版按钮
  486. $("head").append("<style>.edui-btn-auto-format:before{content:\"fix\";}");
  487. (() => {
  488. const $btn = $.eduibutton({
  489. icon: "auto-format",
  490. click: function () {
  491. fnAutoFormat();
  492. },
  493. title: "自动排版",
  494. });
  495. $(".edui-btn-name-insertcode").after($btn);
  496. })();
  497. })();
  498. /* global showdown */
  499. class GM_editor {
  500. $def;
  501. defEditor = null;
  502. htmlContent = "";
  503. $md;
  504. mdEditor = null;
  505. mdContent = "";
  506. defOption = {
  507. init($md) { },
  508. autoSync: false,
  509. curType: "html",
  510. };
  511. option = {};
  512. constructor(option) {
  513. this.option = Object.assign({}, this.defOption, option);
  514. this.init();
  515. this.option.init(this.$md);
  516. this.getContent("html").covert2("md").syncContent("md");
  517. }
  518. init() {
  519. const _this = this;
  520. this.$def = this.option.$defContainer || $(".edui-container");
  521. this.$md = this.createMdEditor();
  522. // 编辑器操作对象
  523. this.defEditor = this.option.defEditor || UM.getEditor("message");
  524. this.mdEditor = {
  525. // 内容变化时触发
  526. addListener(type, fn) {
  527. if (type === "contentChange") {
  528. // _log(_this.$md);
  529. _this.$md.find("#message_md").on("input", fn);
  530. }
  531. },
  532. // 获取内容
  533. getContent() {
  534. return _this.$md.find("#message_md").val();
  535. },
  536. // 写入内容
  537. setContent(content) {
  538. _this.$md.find("#message_md").text(content);
  539. },
  540. };
  541. if (this.option.autoSync) {
  542. this.defEditor.addListener("contentChange", () => {
  543. if (_this.option.curType === "md") {
  544. return;
  545. }
  546. this.getContent("html").covert2("md").syncContent("md");
  547. });
  548. this.mdEditor.addListener("contentChange", () => {
  549. if (_this.option.curType === "html") {
  550. return;
  551. }
  552. this.getContent("md").covert2("html").syncContent("html");
  553. });
  554. }
  555. }
  556. // 读取内容
  557. getContent(type = "html") {
  558. if (type === "html") {
  559. this.htmlContent = this.defEditor.getContent();
  560. } else if (type === "md") {
  561. this.mdContent = this.mdEditor.getContent();
  562. }
  563. return this;
  564. }
  565. // 封装转换函数
  566. covert2(to = "md") {
  567. const converter = new showdown.Converter();
  568. if (to === "md") {
  569. this.mdContent = converter.makeMarkdown(this.htmlContent);
  570. } else if (to === "html") {
  571. this.htmlContent = converter.makeHtml(this.mdContent);
  572. }
  573. return this;
  574. }
  575. // 封装同步函数
  576. syncContent(to = "md") {
  577. if (to === "md") {
  578. this.mdEditor.setContent(this.mdContent);
  579. } else if (to === "html") {
  580. this.defEditor.setContent(this.htmlContent, false);
  581. }
  582. }
  583. // 自动设置 #message_md 的高度
  584. autoSetHeight() {
  585. const $mdText = this.$md.find("#message_md");
  586. // 自动设置高度
  587. $mdText.height(0);
  588. $mdText.height($mdText[0].scrollHeight + 4);
  589. // 判断并绑定 input 事件
  590. if ($mdText.data("bindInput")) {
  591. return;
  592. }
  593. $mdText.data("bindInput", true);
  594. $mdText.on("input", () => {
  595. this.autoSetHeight();
  596. });
  597. }
  598. // 切换编辑器
  599. switchEditor() {
  600. this.$def.toggle();
  601. this.$md.toggle();
  602. // 根据结果设置 curType
  603. this.option.curType = this.$def.css("display") === "none" ? "md" : "html";
  604. // 切换后自动设置高度
  605. this.autoSetHeight();
  606. }
  607. // 创建 markdown 编辑器
  608. createMdEditor() {
  609. return $(`
  610. <div class="mdui-container" style="display: none;">
  611. <div class="mdui-body">
  612. <textarea id="message_md" name="message_md" placeholder="markdown" class="mdui-text"></textarea>
  613. </div>
  614. </div>
  615. `);
  616. }
  617. }
  618. GM_addStyle(`
  619. .is-pulled-right {
  620. float: right;
  621. }
  622. .mdui-container {
  623. border: 1px solid #d4d4d4;
  624. padding: 5px 10px;
  625. }
  626. .mdui-container:focus-within {
  627. border: 1px solid #4caf50;
  628. }
  629. .mdui-text {
  630. border: none;
  631. width: 100%;
  632. min-height: 300px;
  633. height: auto;
  634. }
  635. .mdui-text:focus,
  636. .mdui-text:focus-visible {
  637. outline: none;
  638. box-shadow: none;
  639. }
  640. `);
  641. const mainForBBS = () => {
  642. const gm_editor = new GM_editor({
  643. init($md) {
  644. $(".edui-container").after($md);
  645. },
  646. autoSync: true,
  647. });
  648. const btnSwitchEditor = `
  649. <button class="btn btn-primary" type="button" id="btnSwitchEditor">切换编辑器</button>
  650. `;
  651. // 判断是否有 name 为 fid 的 select
  652. if ($("select[name='fid']").length === 0) {
  653. // quotepid 后追加一行 .form-group
  654. $("input[name='quotepid']").after("<div class=\"form-group\"><span></span></div>");
  655. }
  656. // name 为 quotepid 的 input 下一行追加切换按钮
  657. $("input[name='quotepid'] + .form-group").addClass("d-flex justify-content-between").append(btnSwitchEditor);
  658. // 切换编辑器
  659. $("#btnSwitchEditor").click(() => {
  660. gm_editor.switchEditor();
  661. });
  662. };
  663. const mainForAPP = () => {
  664. const gm_editor = new GM_editor({
  665. init($md) {
  666. $("#editor_content").after($md);
  667. // const $mdText = $md.find("#message_md");
  668. // $mdText.height($("#editor_content").height() - 10);
  669. $(".mdui-body").css({
  670. paddingTop: ".3em",
  671. });
  672. },
  673. $defContainer: $("#editor_content"),
  674. defEditor: UE.getEditor("editor_content"),
  675. autoSync: true,
  676. });
  677. // # cheader 元素内部追加切换按钮
  678. $("#cheader").append(`
  679. <span class="is-pulled-right">「<a href="javascript:;" class="btn btn-primary" id="btnSwitchEditor" title="切换编辑器">切换编辑器</a>」</span>
  680. `);
  681. // 切换编辑器
  682. $("#btnSwitchEditor").click(() => {
  683. gm_editor.switchEditor();
  684. });
  685. };
  686. (() => {
  687. // 判断是否在应用中心编辑页
  688. if (curHref.indexOf("edit.php") > -1) {
  689. // _log(UE)
  690. const editor_api = window.editor_api || unsafeWindow.editor_api;
  691. editor_api.editor.content.obj.ready(mainForAPP);
  692. }
  693. // 判断是否在论坛发帖、回帖页
  694. if ($("textarea#message").length > 0 && $("li.newpost").length === 0) {
  695. mainForBBS();
  696. }
  697. })();
  698. })();