TagBangumiUserPro 班固米用户备注

Based on AttachHowOldtoUserinPosts, to tag users in Bangumi. Now with clickable badges to edit notes.

  1. // ==UserScript==
  2. // @name TagBangumiUserPro 班固米用户备注
  3. // @version 3.4.1
  4. // @description Based on AttachHowOldtoUserinPosts, to tag users in Bangumi. Now with clickable badges to edit notes.
  5. // @author age_anime
  6. // @match https://bgm.tv/*
  7. // @match https://chii.in/*
  8. // @match https://bangumi.tv/*
  9. // @grant none
  10. // @license MIT
  11. // @namespace https://gf.qytechs.cn/users/1426310
  12. // ==/UserScript==
  13.  
  14. (function () {
  15. 'use strict';
  16. // 1.2版本说明:增加详细备注功能,鼠标点击备注可显示详细备注,再次点击备注可以修改备注,支持分别编辑备注和详细备注。删除备注的同时也会清空详细备注。
  17. // 组件函数功能说明:(数据保存在本地,显示和修改在前端,可直接修改和清除)
  18. // displayUserNote(userId, note):在指定用户的用户名旁边显示备注标签
  19. // markUserNotes():显示对应的备注标签
  20. // openUserNoteManager():用户备注管理界面
  21. // openColorManager():颜色管理界面
  22. // addButtonFunctions():添加功能按钮
  23. const defaultNoteColors = {
  24. "预设0": "#40E0D0",
  25. "预设1": "#BB44BB",
  26. "预设2": "#000000",
  27. "预设3": "#DD6D22",
  28. "预设4": "#CC3333",
  29. "预设5": "#DD6D22",
  30. };
  31. let noteColors = JSON.parse(localStorage.getItem('userNoteColors_')) || defaultNoteColors;
  32.  
  33. // 用户备注配置(默认为空白)
  34. let userNoteTable_AGE = JSON.parse(localStorage.getItem('userNoteTable_AGE')) || {};
  35.  
  36. // 迁移旧版本数据结构
  37. function migrateUserNotes() {
  38. let needMigration = false;
  39.  
  40. // 检查是否需要兼容
  41. Object.keys(userNoteTable_AGE).forEach(userId => {
  42. if (typeof userNoteTable_AGE[userId] === 'string') {
  43. needMigration = true;
  44. }
  45. });
  46.  
  47. if (needMigration) {
  48. const newUserNoteTable = {};
  49.  
  50. // 旧版本兼容:兼容v3.4(b1.2)之前的版本
  51. Object.keys(userNoteTable_AGE).forEach(userId => {
  52. if (typeof userNoteTable_AGE[userId] === 'string') {
  53. newUserNoteTable[userId] = [userNoteTable_AGE[userId], ""];
  54. } else {
  55. newUserNoteTable[userId] = userNoteTable_AGE[userId];
  56. }
  57. });
  58.  
  59. userNoteTable_AGE = newUserNoteTable;
  60. localStorage.setItem('userNoteTable_AGE', JSON.stringify(userNoteTable_AGE));
  61. }
  62.  
  63. // 旧版本兼容
  64. Object.keys(localStorage).forEach(key => {
  65. if (key.startsWith('userNote_')) {
  66. const userId = key.substring('userNote_'.length); // 兼容v3.0之前的版本
  67. const noteValue = localStorage.getItem(key);
  68. if (noteValue === "bgmer") { // 兼容v2.7以及之前的版本
  69. localStorage.removeItem(key);
  70. } else {
  71. userNoteTable_AGE[userId] = [noteValue, ""];
  72. localStorage.removeItem(key);
  73. }
  74. }
  75. });
  76. localStorage.setItem('userNoteTable_AGE', JSON.stringify(userNoteTable_AGE));
  77. }
  78.  
  79. migrateUserNotes();
  80.  
  81. const defaultNote = "…";
  82. const defaultColor = "rgba(255, 255, 255, 0)";
  83. const defaultNoteColor = "rgba(102, 170, 85, 0.2)";
  84. const defaultSecondaryNote = "点击修改详细备注...";
  85.  
  86. function displayUserNote(userId, notes) {
  87. const userAnchor = $("strong a.l[href='/user/" + userId + "']");
  88. if (userAnchor.length > 0 && userAnchor.next(".note-badge").length === 0) {
  89. let primaryNote, secondaryNote;
  90.  
  91. if (Array.isArray(notes)) {
  92. [primaryNote, secondaryNote] = notes;
  93. secondaryNote = secondaryNote || "";
  94. } else {
  95. primaryNote = notes;
  96. secondaryNote = "";
  97. }
  98. const badgeColor = (primaryNote === defaultNote) ? defaultColor : (noteColors[primaryNote] || defaultNoteColor);
  99. const noteColorSet = badgeColor === defaultColor ? "#9F9F9F" : "#FFF";
  100.  
  101. const badge = $(`
  102. <span class="note-badge" style="
  103. background: ${badgeColor};
  104. font-size: 10px;
  105. padding: 1px 4px;
  106. color: ${noteColorSet};
  107. border-radius: 6px;
  108. line-height: 150%;
  109. display: inline-block;
  110. cursor: pointer;
  111. position: relative;
  112. " title="双击修改备注">${primaryNote}</span>`);
  113.  
  114. const badgeId = 'note-badge-' + userId + '-' + Math.random().toString(36).substr(2, 9);
  115. badge.attr('id', badgeId);
  116. badge.attr('data-user-id', userId);
  117. badge.attr('data-primary-note', primaryNote);
  118. badge.attr('data-secondary-note', secondaryNote);
  119. badge.attr('data-secondary-loaded', 'false');
  120. // 一级备注点击事件
  121. badge.click(function(e) {
  122. const userId = $(this).attr('data-user-id');
  123. const primaryNote = $(this).attr('data-primary-note');
  124. const storedSecondaryNote = $(this).attr('data-secondary-note');
  125. const secondaryLoaded = $(this).attr('data-secondary-loaded') === 'true';
  126.  
  127. const $target = $(e.target);
  128. const isInteractiveElement = $target.is('a, button, [contenteditable="true"], input, select, textarea');
  129.  
  130. const isSecondaryArea = $target.closest('.secondary-note').length > 0;
  131.  
  132. // 处理二级备注点击
  133. if (isSecondaryArea) {
  134. if (isInteractiveElement) {
  135. return;
  136. }
  137.  
  138. // 处理二级备注编辑
  139. const newSecondaryNote = prompt("请输入详细备注:", storedSecondaryNote || "");
  140. if (newSecondaryNote !== null) {
  141. const trimmedSecondaryNote = newSecondaryNote.trim();
  142. if (primaryNote === defaultNote && trimmedSecondaryNote === "") {
  143. delete userNoteTable_AGE[userId];
  144. localStorage.setItem('userNoteTable_AGE', JSON.stringify(userNoteTable_AGE));
  145. $(this).remove();
  146. } else {
  147. if (Array.isArray(userNoteTable_AGE[userId])) {
  148. userNoteTable_AGE[userId][1] = trimmedSecondaryNote;
  149. } else {
  150. userNoteTable_AGE[userId] = [primaryNote, trimmedSecondaryNote];
  151. }
  152. localStorage.setItem('userNoteTable_AGE', JSON.stringify(userNoteTable_AGE));
  153. $target.closest('.secondary-note').text(trimmedSecondaryNote || defaultSecondaryNote)
  154. .css('color', trimmedSecondaryNote ? '#333' : '#999');
  155. $(this).attr('data-secondary-note', trimmedSecondaryNote);
  156. }
  157. }
  158. e.stopPropagation();
  159. return;
  160. }
  161.  
  162. // 处理一级备注逻辑
  163. let currentSecondaryBadge = $(this).find('.secondary-note');
  164. if (!secondaryLoaded) {
  165. currentSecondaryBadge = $(`
  166. <div class="secondary-note" style="
  167. display: none;
  168. position: absolute;
  169. top: 100%;
  170. left: 0;
  171. width: 200px;
  172. background: white;
  173. border: 1px solid #ccc;
  174. border-radius: 4px;
  175. padding: 5px;
  176. margin-top: 5px;
  177. box-shadow: 0 2px 5px rgba(0,0,0,0.2);
  178. z-index: 1000;
  179. font-size: 12px;
  180. color: ${storedSecondaryNote ? '#333' : '#999'};
  181. cursor: pointer;
  182. max-height: 400px;
  183. overflow-y: auto;
  184. word-wrap: break-word;
  185. resize: both;
  186. " title="点击编辑">${storedSecondaryNote || defaultSecondaryNote}</div>`);
  187. currentSecondaryBadge.on('click', 'a', function(e) {
  188. e.stopPropagation();
  189. });
  190.  
  191. $(this).append(currentSecondaryBadge);
  192. $(this).attr('data-secondary-loaded', 'true');
  193. }
  194.  
  195. if (currentSecondaryBadge.is(':visible')) {
  196. const newNote = prompt("请输入新的备注:", primaryNote);
  197. if (newNote !== null) {
  198. const trimmedNote = newNote.trim();
  199. if (trimmedNote === "" || trimmedNote === defaultNote) {
  200. delete userNoteTable_AGE[userId];
  201. localStorage.setItem('userNoteTable_AGE', JSON.stringify(userNoteTable_AGE));
  202. $(this).remove();
  203. } else if (trimmedNote !== primaryNote) {
  204. if (Array.isArray(userNoteTable_AGE[userId])) {
  205. userNoteTable_AGE[userId][0] = trimmedNote;
  206. } else {
  207. userNoteTable_AGE[userId] = [trimmedNote, storedSecondaryNote];
  208. }
  209. localStorage.setItem('userNoteTable_AGE', JSON.stringify(userNoteTable_AGE));
  210. $(this).contents().first().replaceWith(document.createTextNode(trimmedNote));
  211. const newBadgeColor = noteColors[trimmedNote] || defaultNoteColor;
  212. $(this).css("background", newBadgeColor);
  213. $(this).attr('data-primary-note', trimmedNote);
  214. }
  215. }
  216. } else {
  217. $('.secondary-note').hide();
  218. currentSecondaryBadge.show();
  219. }
  220. });
  221.  
  222. // 点击其他区域关闭二级备注
  223. if (!window.noteClickHandlerAdded) {
  224. $(document).on('click', function(e) {
  225. if (!$(e.target).closest('.note-badge').length) {
  226. $('.secondary-note').hide();
  227. }
  228. });
  229. window.noteClickHandlerAdded = true;
  230. }
  231.  
  232. userAnchor.after(badge);
  233. }
  234. }
  235.  
  236. function markUserNotes() {
  237. $(".note-badge").remove();
  238. $("strong a.l:not(.avatar)").each(function () {
  239. const userLink = $(this).attr("href");
  240. const userId = userLink.split("/").pop();
  241. const notes = userNoteTable_AGE[userId] || defaultNote;
  242. displayUserNote(userId, notes);
  243. });
  244. }
  245.  
  246. // 按钮颜色管理区
  247. function openColorManager() {
  248. const modal = document.createElement('div');
  249. Object.assign(modal.style, {
  250. position: 'fixed',
  251. left: '50%',
  252. top: '50%',
  253. transform: 'translate(-50%, -50%)',
  254. backgroundColor: 'white',
  255. padding: '20px',
  256. borderRadius: '5px',
  257. boxShadow: '0 0 10px rgba(0,0,0,0.5)',
  258. zIndex: '9999',
  259. width: '300px'
  260. });
  261.  
  262. // 添加颜色
  263. const form = document.createElement('div');
  264. form.style.marginBottom = '5px';
  265.  
  266. const nameLabel = document.createElement('label');
  267. nameLabel.textContent = '名称:';
  268. nameLabel.style.color = '#333333';
  269. nameLabel.style.marginRight = '5px';
  270.  
  271. const nameInput = document.createElement('input');
  272. nameInput.type = 'text';
  273. nameInput.style.minWidth = '240px';
  274. nameInput.style.width = 'auto';
  275. nameInput.style.height = '22px';
  276. nameInput.style.marginRight = '5px';
  277. nameInput.style.position = 'relative';
  278. nameInput.style.top = '-5px';
  279.  
  280. const colorLabel = document.createElement('label');
  281. colorLabel.textContent = '颜色:';
  282. colorLabel.style.color = '#333333';
  283. colorLabel.style.marginRight = '5px';
  284.  
  285. const colorInput = document.createElement('input');
  286. colorInput.type = 'color';
  287. colorInput.style.marginRight = '5px';
  288.  
  289. const addButton = document.createElement('button');
  290. addButton.textContent = '添加';
  291. addButton.onclick = () => {
  292. const name = nameInput.value.trim();
  293. const color = colorInput.value;
  294. if (name && color) {
  295. noteColors[name] = color;
  296. textarea.value = JSON.stringify(noteColors, null, 4);
  297. nameInput.value = '';
  298. colorInput.value = '#000000';
  299. } else {
  300. alert('名称和颜色不能为空!');
  301. }
  302. };
  303.  
  304. form.append(nameLabel, nameInput, colorLabel, colorInput, addButton);
  305.  
  306. const textarea = document.createElement('textarea');
  307. textarea.style.cssText = 'width: 100%; height: 200px; margin-bottom: 10px;';
  308. textarea.value = JSON.stringify(noteColors, null, 4);
  309.  
  310. const buttonContainer = document.createElement('div');
  311. buttonContainer.style.cssText = 'text-align: right;';
  312.  
  313. const cancelBtn = document.createElement('button');
  314. cancelBtn.textContent = '取消';
  315. cancelBtn.onclick = () => modal.remove();
  316.  
  317. const saveBtn = document.createElement('button');
  318. saveBtn.textContent = '保存';
  319. saveBtn.style.marginLeft = '8px';
  320. saveBtn.onclick = () => {
  321. try {
  322. const newColors = JSON.parse(textarea.value);
  323. if (typeof newColors !== 'object' || newColors === null) {
  324. throw new Error('请输入有效的JSON对象');
  325. }
  326. localStorage.setItem('userNoteColors_', JSON.stringify(newColors));
  327. noteColors = newColors;
  328. document.querySelectorAll('.note-badge').forEach(badge => {
  329. const note = badge.textContent;
  330. badge.style.backgroundColor = noteColors[note] || defaultNoteColor;
  331. });
  332. modal.remove();
  333. } catch (error) {
  334. alert('错误: 请检查JSON格式(其他行的末尾是有英文逗号的,最后一行的末尾是没有逗号的!),还不会就把错误代码和JSON内容放在AI里面问问: ' + error.message);
  335. }
  336. };
  337.  
  338. buttonContainer.append(cancelBtn, saveBtn);
  339. modal.append(form, textarea, buttonContainer);
  340. document.body.appendChild(modal);
  341. }
  342. // 用户备注管理区
  343. function openUserNoteManager() {
  344. const modal = document.createElement('div');
  345. Object.assign(modal.style, {
  346. position: 'fixed',
  347. left: '50%',
  348. top: '50%',
  349. transform: 'translate(-50%, -50%)',
  350. backgroundColor: 'white',
  351. padding: '20px',
  352. borderRadius: '5px',
  353. boxShadow: '0 0 10px rgba(0,0,0,0.5)',
  354. zIndex: '9999',
  355. width: '300px'
  356. });
  357.  
  358. // 添加上方输入框
  359. const form = document.createElement('div');
  360. form.style.marginBottom = '10px';
  361.  
  362. const idInput = document.createElement('input');
  363. idInput.placeholder = '用户ID';
  364. idInput.style.marginRight = '5px';
  365.  
  366. const noteInput = document.createElement('input');
  367. noteInput.placeholder = '备注';
  368. noteInput.style.marginRight = '5px';
  369.  
  370. const secondaryNoteInput = document.createElement('input');
  371. secondaryNoteInput.placeholder = '详细备注(可选)';
  372. secondaryNoteInput.style.marginRight = '5px';
  373. secondaryNoteInput.style.width = '100%';
  374.  
  375. const addButton = document.createElement('button');
  376. addButton.textContent = '添加';
  377. addButton.onclick = () => {
  378. const userId = idInput.value.trim();
  379. const note = noteInput.value.trim();
  380. const secondaryNote = secondaryNoteInput.value.trim();
  381. if (userId && note) {
  382. userNoteTable_AGE[userId] = [note, secondaryNote];
  383. textarea.value = JSON.stringify(userNoteTable_AGE, null, 4);
  384. idInput.value = '';
  385. noteInput.value = '';
  386. secondaryNoteInput.value = '';
  387. }
  388. };
  389.  
  390. form.append(idInput, noteInput, document.createElement('br'), secondaryNoteInput, addButton);
  391. // JSON编辑区
  392. const textarea = document.createElement('textarea');
  393. textarea.style.cssText = 'width: 100%; height: 200px; margin: 10px 0;';
  394. textarea.value = JSON.stringify(userNoteTable_AGE, null, 4);
  395. const buttonContainer = document.createElement('div');
  396. buttonContainer.style.cssText = 'text-align: right;';
  397. const importBtn = document.createElement('button');
  398. importBtn.textContent = '导入';
  399. importBtn.onclick = () => {
  400. const input = document.createElement("input");
  401. input.type = "file";
  402. input.accept = "application/json";
  403. input.onchange = function (event) {
  404. const file = event.target.files[0];
  405. if (file) {
  406. const reader = new FileReader();
  407. reader.onload = function (e) {
  408. try {
  409. const imported = JSON.parse(e.target.result);
  410. userNoteTable_AGE = imported; // 直接替换当前内容
  411. textarea.value = JSON.stringify(userNoteTable_AGE, null, 4);
  412. localStorage.setItem('userNoteTable_AGE', JSON.stringify(userNoteTable_AGE));
  413. markUserNotes(); // 刷新页面显示
  414. alert("导入成功!");
  415. } catch (error) {
  416. alert("错误: 请检查JSON格式(其他行的末尾是有英文逗号的,最后一行的末尾是没有逗号的!),还不会就把错误代码和JSON内容放在AI里面询问:" + error);
  417. }
  418. };
  419. reader.readAsText(file);
  420. }
  421. };
  422. input.click();
  423. };
  424. const exportBtn = document.createElement('button');
  425. exportBtn.textContent = '导出';
  426. exportBtn.style.marginLeft = '5px';
  427. exportBtn.onclick = () => {
  428. const blob = new Blob([textarea.value], { type: 'application/json' });
  429. const url = URL.createObjectURL(blob);
  430. const a = document.createElement('a');
  431. a.href = url;
  432. a.download = `user_notes_${Date.now()}.json`;
  433. a.click();
  434. URL.revokeObjectURL(url);
  435. };
  436.  
  437. const cancelBtn = document.createElement('button');
  438. cancelBtn.textContent = '取消';
  439. cancelBtn.style.marginLeft = '5px';
  440. cancelBtn.onclick = () => modal.remove();
  441.  
  442. const saveBtn = document.createElement('button');
  443. saveBtn.textContent = '保存';
  444. saveBtn.style.marginLeft = '5px';
  445. saveBtn.onclick = () => {
  446. try {
  447. const newNotes = JSON.parse(textarea.value);
  448. localStorage.setItem('userNoteTable_AGE', JSON.stringify(newNotes));
  449. userNoteTable_AGE = newNotes;
  450. markUserNotes();
  451. modal.remove();
  452. } catch (error) {
  453. alert('JSON格式错误:' + error.message);
  454. }
  455. };
  456.  
  457. buttonContainer.append(importBtn, exportBtn, cancelBtn, saveBtn);
  458. modal.append(form, textarea, buttonContainer);
  459. document.body.appendChild(modal);
  460. }
  461.  
  462. function addButtonFunctions() {
  463. const badgeUserPanel = document.querySelector("ul#badgeUserPanel");
  464. if (badgeUserPanel) {
  465. // 用户备注管理按钮
  466. const userNoteLi = document.createElement('li');
  467. const userNoteBtn = document.createElement('a');
  468. userNoteBtn.href = '#';
  469. userNoteBtn.textContent = '管理用户备注';
  470. userNoteBtn.onclick = (e) => { e.preventDefault(); openUserNoteManager(); };
  471. userNoteLi.appendChild(userNoteBtn);
  472. // 颜色管理按钮
  473. const colorLi = document.createElement('li');
  474. const colorBtn = document.createElement('a');
  475. colorBtn.href = '#';
  476. colorBtn.textContent = '管理颜色配置';
  477. colorBtn.onclick = (e) => { e.preventDefault(); openColorManager(); };
  478. colorLi.appendChild(colorBtn);
  479.  
  480. badgeUserPanel.append(userNoteLi, colorLi);
  481. }
  482. }
  483. if (document.body) {
  484. markUserNotes();
  485. } else {
  486. window.onload = markUserNotes;
  487. }
  488. setTimeout(addButtonFunctions, 200);
  489. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址