Duoshuo Admin

更加强大的多说评论管理脚本,可以自动分析所有有评论的页面,方便地修改thread_key,避免评论丢失。

  1. // ==UserScript==
  2. // @name Duoshuo Admin
  3. // @name:zh-CN 多说评论管理
  4. // @namespace http://gerald.top
  5. // @homepage https://gerald.top/code/DuoshuoAdmin
  6. // @author Gerald <i@gerald.top>
  7. // @icon http://cn.gravatar.com/avatar/a0ad718d86d21262ccd6ff271ece08a3?s=80
  8. // @description 更加强大的多说评论管理脚本,可以自动分析所有有评论的页面,方便地修改thread_key,避免评论丢失。
  9. // @version 0.3
  10. // @match http://*.duoshuo.com/admin/*
  11. // @grant none
  12. // ==/UserScript==
  13.  
  14. var threads;
  15. try {
  16. threads = JSON.parse(localStorage.threadData);
  17. } catch(e) {
  18. threads = [];
  19. }
  20. function save() {
  21. localStorage.threadData = JSON.stringify(threads);
  22. }
  23.  
  24. $('<style>')
  25. .html(`
  26. .dsa-popup{background:white;position:fixed;top:70px;right:70px;padding:10px;border-radius:3px;box-shadow:0 0 5px rgba(0,0,0,.1);}
  27. .dsa-dialog{background:white;position:absolute;top:50px;right:600px;width:400px;z-index:10;box-shadow:0 0 5px rgba(0,0,0,.1);border:1px solid gray;padding:10px;border-radius:3px;}
  28. .dsa-dialog label{display:block;}
  29. .dsa-dialog input[type=text],.dsa-dialog input[type=url]{display:block;width:100%;}
  30. .dsa-panel{background:white;position:fixed;top:50px;right:0;bottom:0;width:600px;box-shadow:-2px 0 5px gray;}
  31. .dsa-panel-body{position:absolute;top:30px;left:10px;right:10px;bottom:10px;overflow:auto;}
  32. .dsa-panel table{width:100%;border-spacing:5px;border-collapse:separate;table-layout:fixed;}
  33. .dsa-panel td>*{padding:5px;}
  34. .dsa-panel .key{cursor:pointer;}
  35. .dsa-panel .key:hover{background:olive;color:white;}
  36. `)
  37. .appendTo(document.head);
  38.  
  39. const menu = $('<ul class="dropdown-menu">');
  40. $('<li class="dropdown">')
  41. .prependTo('.navbar-nav')
  42. .html('<a href class="dropdown-toggle" data-toggle="dropdown">评论管理<b class="caret"></b>')
  43. .append(menu);
  44.  
  45. $('<li><a href="https://gerald.top/code/DuoshuoAdmin" target=_blank>使用须知</a></li>')
  46. .appendTo(menu);
  47.  
  48. function EditPanel(thread) {
  49. this.thread = thread;
  50. [
  51. 'onSubmit',
  52. 'onClose',
  53. 'onVisitURL',
  54. 'onSolveConflict',
  55. ].forEach(key => {
  56. this[key] = this[key].bind(this);
  57. });
  58. this.dialog = $('<form class="dsa-dialog">').appendTo(document.body)
  59. .html(`
  60. <h3>修改Thread</h3>
  61. <label>
  62. thread_key:
  63. <input type=text data-id=thread_key>
  64. </label>
  65. <label>
  66. title:
  67. <input type=text data-id=title>
  68. </label>
  69. <label>
  70. url: <a href="#" data-id=visit_url>[Go]</a>
  71. <input type=url data-id=url>
  72. </label>
  73. <label>
  74. <input type=checkbox data-id=post_enable>
  75. post_enable
  76. </label>
  77. <button class="btn btn-primary" type="submit">保存</button>
  78. <button class="btn btn-default" type="button" data-id=cancel>关闭</button>
  79. <button class="btn btn-default" data-id=conflict style="display:none">修改冲突</button>
  80. <div data-id=msg></div>
  81. `)
  82. .submit(this.onSubmit)
  83. .on('click', '[data-id=visit_url]', this.onVisitURL)
  84. .on('click', '[data-id=cancel]', this.onClose)
  85. .on('click', '[data-id=conflict]', this.onSolveConflict)
  86. ;
  87. this.find('thread_key').val(thread.thread_key);
  88. this.find('title').val(thread.title);
  89. this.find('url').val(thread.url);
  90. this.find('post_enable').prop('checked', thread.post_enable);
  91. }
  92. EditPanel.panels = {};
  93. EditPanel.open = function (thread) {
  94. var panel = EditPanel.panels[thread.thread_id];
  95. if (!panel) {
  96. panel = EditPanel.panels[thread.thread_id] = new EditPanel(thread);
  97. }
  98. panel.dialog.appendTo(document.body);
  99. return panel;
  100. };
  101. EditPanel.prototype.find = function (key) {
  102. return this.dialog.find(`[data-id=${JSON.stringify(key)}]`);
  103. };
  104. EditPanel.prototype.onSubmit = function (e) {
  105. e.preventDefault();
  106. $.post('/api/threads/update.json', {
  107. thread_id: this.thread.thread_id,
  108. thread_key: this.find('thread_key').val(),
  109. title: this.find('title').val(),
  110. url: this.find('url').val(),
  111. post_enable: this.find('post_enable').prop('checked') ? 1 : 0,
  112. }, r => {
  113. const thread = r.response;
  114. if (thread) {
  115. this.thread = thread;
  116. const i = threads.findIndex(_thread => _thread.thread_id === thread.thread_id);
  117. if (~i) threads[i] = thread;
  118. save();
  119. this.find('msg').html('修改成功!');
  120. } else {
  121. this.find('msg').html('<span style="color:red">修改失败!</span>');
  122. const conflict = this.find('conflict').show();
  123. this.find('thread_key').one('change', () => conflict.hide());
  124. }
  125. });
  126. };
  127. EditPanel.prototype.onVisitURL = function (e) {
  128. window.open(this.find('url').val());
  129. };
  130. EditPanel.prototype.onSolveConflict = function (e) {
  131. $.get('/api/threads/listPosts.json', {
  132. thread_key: this.find('thread_key').val(),
  133. limit: 1,
  134. }, r => {
  135. EditPanel.open(r.thread);
  136. });
  137. };
  138. EditPanel.prototype.onClose = function (e) {
  139. this.dialog.remove();
  140. };
  141.  
  142. function getThreadByKey(key, cb) {
  143. }
  144. var panel=$('<div class="dsa-panel">').appendTo(document.body).hide();
  145. $('<a href>关闭</a>').appendTo(panel).css({
  146. position:'absolute',
  147. top:10,
  148. right:20,
  149. }).click(function(e){
  150. e.preventDefault();
  151. panel.hide();
  152. });
  153. panel.area=$('<div class="dsa-panel-body">').appendTo(panel);
  154. $('<a href>显示文章列表</a>')
  155. .appendTo($('<li>').appendTo(menu))
  156. .click(function(e){
  157. e.preventDefault();
  158. panel.area.html('');
  159. var table=$('<table>').appendTo(panel.area);
  160. $('<colgroup><col width=100><col width=200><col></colgroup>').appendTo(table);
  161. $('<tr><th>thread_key</th><th>title</th><th>url</th></tr>').appendTo(table);
  162. threads.forEach(thread => {
  163. const row = $('<tr>').appendTo(table);
  164. $('<span class="key">')
  165. .text(thread.thread_key)
  166. .click(() => EditPanel.open(thread))
  167. .wrap('<td>').parent().appendTo(row);
  168. $('<td>').text(thread.title).appendTo(row);
  169. $('<a target=_blank>')
  170. .attr('href', thread.url)
  171. .text(thread.url)
  172. .wrap('<td>').parent().appendTo(row);
  173. });
  174. panel.show();
  175. });
  176.  
  177. $('<a href>自动分析评论</a>')
  178. .appendTo($('<li>').appendTo(menu))
  179. .click(function(e){
  180. e.preventDefault();
  181. var dthreads={};
  182. var limit=100,n=~~(DUOSHUO.site.comments/limit),i;
  183. var finished=0;
  184. var popup=$('<div class="dsa-popup">').html('正在分析评论:<span></span>/'+n).appendTo(document.body);
  185. var prog=popup.find('span').html(finished);
  186. for(i=0;i<n;i++)
  187. $.get('/api/posts/list.json',{
  188. order:'desc',
  189. source:'duoshuo,repost',
  190. max_depth:1,
  191. limit:limit,
  192. 'related[]':'thread',
  193. nonce:DUOSHUO.nonce,
  194. status:'all',
  195. page:i+1,
  196. },function(r){
  197. $.each(r.response,function(i,p){
  198. var t=p.thread;
  199. dthreads[t.thread_id]=t;
  200. });
  201. prog.html(++finished);
  202. if(finished==n) {
  203. popup.html('评论分析完成!');
  204. popup.fadeOut(5000,function(){
  205. popup.remove();
  206. });
  207. threads=[];
  208. for(var i in dthreads) threads.push(dthreads[i]);
  209. threads.sort(function(a,b){return a.thread_id>b.thread_id;});
  210. save();
  211. }
  212. });
  213. });