@jx3box/jx3box-ui 2.0.5 → 2.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (241) hide show
  1. package/.storybook/preview.js +22 -13
  2. package/.storybook/storybookMocks.js +214 -0
  3. package/README.md +39 -43
  4. package/assets/css/bread/admin.less +20 -2
  5. package/assets/css/bread/breadcrumb.less +5 -2
  6. package/assets/css/comment/quick-reply.less +1 -1
  7. package/assets/css/common/left-sidebar.less +1 -1
  8. package/assets/css/interact/like2.less +5 -2
  9. package/assets/css/interact/qrcode.less +69 -0
  10. package/assets/css/single/author.less +15 -10
  11. package/assets/css/single/cms-single.less +1 -0
  12. package/assets/css/single/thx.less +34 -11
  13. package/assets/css/wiki/wiki-comments.less +176 -0
  14. package/assets/css/wiki/wiki-panel.less +177 -0
  15. package/assets/css/wiki/wiki-revisions.less +62 -0
  16. package/assets/data/nav.json +344 -10
  17. package/assets/img/comment/comment.svg +1 -1
  18. package/assets/img/comment/heart_1.svg +1 -1
  19. package/assets/img/comment/hide.svg +1 -1
  20. package/assets/img/leftsidebar/medal.svg +1 -1
  21. package/assets/img/leftsidebar/post.svg +27 -46
  22. package/i18n/messages/en-US.js +357 -22
  23. package/i18n/messages/vi.js +350 -22
  24. package/i18n/messages/zh-CN.js +469 -20
  25. package/i18n/messages/zh-TW.js +350 -22
  26. package/index.js +19 -7
  27. package/package.json +8 -5
  28. package/service/cms.js +22 -0
  29. package/service/commit-history.js +18 -0
  30. package/service/community.js +60 -0
  31. package/service/fav.js +5 -6
  32. package/src/App.vue +210 -80
  33. package/src/Breadcrumb.vue +24 -11
  34. package/src/CommonFooter.vue +2 -2
  35. package/src/Footer.vue +13 -5
  36. package/src/LeftSideToggle.vue +10 -3
  37. package/src/LeftSidebar.vue +19 -5
  38. package/src/Main.vue +9 -2
  39. package/src/RightSidebar.vue +12 -3
  40. package/src/SuspendCommon.vue +52 -38
  41. package/src/author/AuthorFans.vue +7 -5
  42. package/src/author/AuthorFollow.vue +14 -7
  43. package/src/author/AuthorGift.vue +25 -17
  44. package/src/author/AuthorInfo.vue +46 -19
  45. package/src/author/AuthorLink.vue +3 -1
  46. package/src/author/AuthorMedals.vue +5 -2
  47. package/src/author/AuthorPosts.vue +31 -13
  48. package/src/author/AuthorRss.vue +7 -3
  49. package/src/author/AuthorTeams.vue +3 -1
  50. package/src/author/UserPop.vue +29 -17
  51. package/src/bread/Admin.vue +55 -32
  52. package/src/bread/AdminDirectMessage.vue +15 -8
  53. package/src/bread/AdminDrop.vue +124 -45
  54. package/src/bread/CommunityAdmin.vue +527 -0
  55. package/src/bread/DesignTask.vue +30 -26
  56. package/src/bread/ListAdmin.vue +199 -0
  57. package/src/bread/MigrateCommunity.vue +192 -0
  58. package/src/bread/MoveToCommunityDialog.vue +234 -0
  59. package/src/comment/Avatar.vue +18 -24
  60. package/src/comment/CommentContent.vue +57 -37
  61. package/src/comment/CommentContentSimple.vue +61 -63
  62. package/src/comment/CommentInputForm.vue +45 -34
  63. package/src/comment/CommentWithReply.vue +9 -6
  64. package/src/comment/QuickReply.vue +5 -1
  65. package/src/comment/ReplyForReply.vue +6 -4
  66. package/src/comment/ReplyList.vue +28 -18
  67. package/src/comment/Upload.vue +24 -6
  68. package/src/filters/clientBy.vue +10 -8
  69. package/src/filters/markBy.vue +35 -31
  70. package/src/filters/menuBy.vue +20 -22
  71. package/src/filters/orderBy.vue +17 -11
  72. package/src/filters/tagBy.vue +4 -2
  73. package/src/filters/topicBy.vue +12 -10
  74. package/src/filters/versionBy.vue +5 -3
  75. package/src/filters/zlpBy.vue +29 -25
  76. package/src/footer/copyright.vue +5 -5
  77. package/src/footer/links.vue +2 -2
  78. package/src/footer/official.vue +1 -1
  79. package/src/footer/resource.vue +6 -6
  80. package/src/header/alternate.vue +35 -18
  81. package/src/header/asset.vue +9 -19
  82. package/src/header/box.vue +20 -8
  83. package/src/header/box2.vue +16 -9
  84. package/src/header/client.vue +7 -7
  85. package/src/header/lang.vue +41 -6
  86. package/src/header/logo.vue +2 -3
  87. package/src/header/manage.vue +2 -2
  88. package/src/header/message.vue +3 -3
  89. package/src/header/nav.vue +26 -23
  90. package/src/header/publish.vue +2 -2
  91. package/src/header/search.vue +2 -2
  92. package/src/header/shop.vue +2 -2
  93. package/src/header/user.vue +4 -4
  94. package/src/header/userInfo.vue +22 -24
  95. package/src/header/vip.vue +2 -2
  96. package/src/interact/BoxcoinAdmin.vue +30 -16
  97. package/src/interact/BoxcoinRecords.vue +28 -11
  98. package/src/interact/BoxcoinUser.vue +33 -17
  99. package/src/interact/Fav.vue +5 -1
  100. package/src/interact/Homework.vue +317 -0
  101. package/src/interact/Like.vue +4 -2
  102. package/src/interact/QRcode.vue +104 -0
  103. package/src/interact/Rss.vue +10 -4
  104. package/src/interact/Share.vue +18 -19
  105. package/src/interact/WatchLater.vue +5 -1
  106. package/src/main.js +1 -0
  107. package/src/single/Author.vue +5 -1
  108. package/src/single/CmsSingle.vue +29 -15
  109. package/src/single/Collection.vue +36 -26
  110. package/src/single/Comment.vue +19 -13
  111. package/src/single/Creators.vue +5 -3
  112. package/src/single/PostCollection.vue +158 -0
  113. package/src/single/PostDirectory.vue +0 -126
  114. package/src/single/PostGuide.vue +93 -0
  115. package/src/single/PostHeader.vue +39 -17
  116. package/src/single/PostTopic.vue +3 -3
  117. package/src/single/PostVersion.vue +219 -0
  118. package/src/single/RightAffix.vue +160 -0
  119. package/src/single/SimpleThx.vue +4 -2
  120. package/src/single/Thx.vue +14 -2
  121. package/src/single/VersionDialog.vue +238 -0
  122. package/src/stories/components/StoryPropsTable.vue +137 -0
  123. package/src/stories/filters/FilterGallery.stories.js +110 -0
  124. package/src/stories/filters/clientBy.stories.js +24 -0
  125. package/src/stories/filters/markBy.stories.js +6 -0
  126. package/src/stories/filters/menuBy.stories.js +6 -0
  127. package/src/stories/filters/orderBy.stories.js +6 -0
  128. package/src/stories/filters/tagBy.stories.js +6 -0
  129. package/src/stories/filters/topicBy.stories.js +6 -0
  130. package/src/stories/filters/versionBy.stories.js +6 -0
  131. package/src/stories/filters/zlpBy.stories.js +6 -0
  132. package/src/stories/interact/QRcode.stories.js +59 -0
  133. package/src/stories/layout/BaseShell.stories.js +38 -0
  134. package/src/stories/layout/Breadcrumb.stories.js +40 -0
  135. package/src/stories/layout/CommonFooter.stories.js +1 -1
  136. package/src/stories/layout/CommonHeader.stories.js +1 -1
  137. package/src/stories/layout/LeftSideToggle.stories.js +78 -0
  138. package/src/stories/layout/LeftSidebar.stories.js +40 -0
  139. package/src/stories/layout/Main.stories.js +41 -0
  140. package/src/stories/layout/RightSidebar.stories.js +36 -0
  141. package/src/stories/layout/StorybookShell.vue +271 -0
  142. package/src/stories/layout/SuspendCommon.stories.js +67 -0
  143. package/src/stories/layout/SuspendCommonStage.vue +143 -0
  144. package/src/stories/mockData.js +369 -0
  145. package/src/stories/other/UserPop.stories.js +51 -0
  146. package/src/stories/single/Author.stories.js +19 -0
  147. package/src/stories/single/AuthorStoryStage.vue +103 -0
  148. package/src/stories/single/CmsSingle.stories.js +19 -0
  149. package/src/stories/single/CmsSingleStoryStage.vue +118 -0
  150. package/src/stories/single/Collection.stories.js +34 -0
  151. package/src/stories/single/Comment.stories.js +36 -0
  152. package/src/stories/single/Creators.stories.js +34 -0
  153. package/src/stories/single/PostCollection.stories.js +34 -0
  154. package/src/stories/single/PostDirectory.stories.js +31 -0
  155. package/src/stories/single/PostGuide.stories.js +47 -0
  156. package/src/stories/single/PostHeader.stories.js +39 -0
  157. package/src/stories/single/PostVersion.stories.js +41 -0
  158. package/src/stories/single/RightAffix.stories.js +84 -0
  159. package/src/stories/single/SimpleThx.stories.js +39 -0
  160. package/src/stories/single/SingleGallery.stories.js +207 -0
  161. package/src/stories/single/Thx.stories.js +38 -0
  162. package/src/stories/wiki/GamePrice.stories.js +64 -0
  163. package/src/stories/wiki/WikiComments.stories.js +58 -0
  164. package/src/stories/wiki/WikiPanel.stories.js +69 -0
  165. package/src/stories/wiki/WikiRevisions.stories.js +47 -0
  166. package/src/wiki/GamePrice.vue +107 -0
  167. package/src/wiki/WikiComment.vue +153 -0
  168. package/src/wiki/WikiComments.vue +206 -0
  169. package/src/wiki/WikiPanel.vue +121 -0
  170. package/src/wiki/WikiRevisions.vue +125 -0
  171. package/vue.config.js +2 -12
  172. package/assets/css/common/bottom.less +0 -25
  173. package/assets/css/editor/article.less +0 -36
  174. package/assets/css/editor/combo.less +0 -362
  175. package/assets/css/editor/resource.less +0 -399
  176. package/assets/css/editor/tinymce.less +0 -100
  177. package/assets/css/editor/upload.less +0 -136
  178. package/assets/css/module/author.less +0 -231
  179. package/assets/css/module/buff.less +0 -88
  180. package/assets/css/module/directory.less +0 -165
  181. package/assets/css/module/icon.less +0 -25
  182. package/assets/css/module/item.less +0 -279
  183. package/assets/css/module/item_simple.less +0 -58
  184. package/assets/css/module/jx3_element.less +0 -5
  185. package/assets/css/module/macro.less +0 -89
  186. package/assets/css/module/npc.less +0 -265
  187. package/assets/css/module/resource.less +0 -93
  188. package/assets/css/module/skill.less +0 -74
  189. package/assets/css/module/talent.less +0 -495
  190. package/assets/js/a.js +0 -46
  191. package/assets/js/code.js +0 -13
  192. package/assets/js/combo.js +0 -40
  193. package/assets/js/directory.js +0 -114
  194. package/assets/js/filter2.js +0 -186
  195. package/assets/js/fold.js +0 -11
  196. package/assets/js/gallery.js +0 -14
  197. package/assets/js/hljs_languages.js +0 -177
  198. package/assets/js/iframe.js +0 -38
  199. package/assets/js/img.js +0 -23
  200. package/assets/js/item/attribute_percent.js +0 -88
  201. package/assets/js/item/bind.js +0 -18
  202. package/assets/js/item/border.js +0 -19
  203. package/assets/js/item/border_quest.js +0 -12
  204. package/assets/js/item/color.js +0 -25
  205. package/assets/js/item/hljs_languages.js +0 -177
  206. package/assets/js/item/icon_url.js +0 -15
  207. package/assets/js/item/second_format.js +0 -19
  208. package/assets/js/jx3_element.js +0 -73
  209. package/assets/js/katex.js +0 -181
  210. package/assets/js/macro.js +0 -16
  211. package/assets/js/nextpage.js +0 -4
  212. package/assets/js/pswp.js +0 -67
  213. package/assets/js/pswp_template.js +0 -66
  214. package/assets/js/pz_iframe.js +0 -30
  215. package/assets/js/qixue.js +0 -28
  216. package/assets/js/renderImgPreview.js +0 -18
  217. package/assets/js/script.js +0 -6
  218. package/assets/js/stat.js +0 -107
  219. package/assets/js/talent2.js +0 -21
  220. package/assets/js/tex-mml-chtml.js +0 -36310
  221. package/assets/js/voice.js +0 -238
  222. package/assets/js/xss.js +0 -130
  223. package/src/editor/Article.vue +0 -230
  224. package/src/editor/BoxResource.vue +0 -441
  225. package/src/editor/Resource.vue +0 -623
  226. package/src/editor/Tinymce.vue +0 -204
  227. package/src/editor/Upload.vue +0 -277
  228. package/src/editor/UploadAlum.vue +0 -176
  229. package/src/editor/UploadBanner.vue +0 -156
  230. package/src/editor/components/Author.vue +0 -322
  231. package/src/editor/components/Avatar.vue +0 -95
  232. package/src/editor/components/Buff.vue +0 -80
  233. package/src/editor/components/Combo.vue +0 -259
  234. package/src/editor/components/Equip.vue +0 -299
  235. package/src/editor/components/GameText.vue +0 -242
  236. package/src/editor/components/Item.vue +0 -325
  237. package/src/editor/components/ItemSimple.vue +0 -134
  238. package/src/editor/components/Medal.vue +0 -43
  239. package/src/editor/components/Npc.vue +0 -215
  240. package/src/editor/components/PostAuthor.vue +0 -102
  241. package/src/editor/components/Skill.vue +0 -71
@@ -1,238 +0,0 @@
1
- import $ from "jquery";
2
- import { showAvatar } from "@jx3box/jx3box-common/js/utils";
3
-
4
- /**
5
- * 渲染音频组件
6
- * 将 e-audio 转换为实际的音频播放器
7
- * @param {string} selector - 选择器,默认为 ".w-audio, .e-audio"
8
- */
9
- function renderVoice(selector = ".w-audio, .e-audio") {
10
- try {
11
- $(selector).each(function (i, ele) {
12
- const $audio = $(this);
13
- const content = $audio.text().trim();
14
-
15
- // 解析内容:name:xxx;author:xxx;user_id:xxx;src:xxx
16
- const params = {};
17
- content.split(";").forEach((item) => {
18
- const [key, value] = item.split("|");
19
- if (key && value !== undefined) {
20
- params[key.trim()] = value.trim();
21
- }
22
- });
23
-
24
- // 提取参数
25
- const { name = "未命名音频", author = "未知", user_id = "", src = "" } = params;
26
-
27
- if (!src) {
28
- console.warn("音频源地址为空", content);
29
- return;
30
- }
31
-
32
- // 生成唯一ID
33
- const playerId = `audio-player-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
34
-
35
- // 获取用户头像,如果没有则使用默认图片
36
- const avatar = showAvatar(params.avatar, 240);
37
-
38
- // 渲染音频播放器 - 使用项目中定义的样式结构
39
- const html = `
40
- <div class="w-audio-player" id="${playerId}" data-user-id="${user_id}">
41
- <div class="m-item">
42
- <div class="u-title">
43
- <div class="clip">
44
- <div class="marquee-wrapper">
45
- <span class="marquee-text">${name}</span>
46
- </div>
47
- </div>
48
- </div>
49
- <div class="u-author">
50
- 练习生:<a href="https://www.jx3box.com/author/${user_id}" target="_blank">${author}</a>
51
- </div>
52
-
53
- <div class="m-record">
54
- <img class="u-needle" src="https://cdn.jx3box.com/design/event/jx3cxk/web/item/needle.svg" />
55
- <a href="https://www.jx3box.com/author/${user_id}" target="_blank" class="u-record">
56
- <img class="u-avatar" src="${avatar}" />
57
- </a>
58
- </div>
59
-
60
- <div class="m-progress">
61
- <div class="u-progress-bar">
62
- <div class="u-progress-fill"></div>
63
- <div class="u-progress-handle"></div>
64
- </div>
65
- </div>
66
-
67
- <div class="m-play">
68
- <div class="u-play-button">
69
- <img class="u-icon" src="https://cdn.jx3box.com/design/event/jx3cxk/web/item/left.svg" />
70
- <img class="u-icon u-play" src="https://cdn.jx3box.com/design/event/jx3cxk/web/item/play.svg" data-play-icon="https://cdn.jx3box.com/design/event/jx3cxk/web/item/play.svg" data-stop-icon="https://cdn.jx3box.com/design/event/jx3cxk/web/item/stop.svg" />
71
- <img class="u-icon" src="https://cdn.jx3box.com/design/event/jx3cxk/web/item/right.svg" />
72
- </div>
73
- </div>
74
- </div>
75
- <audio preload="metadata" src="${src}"></audio>
76
- </div>
77
- `;
78
-
79
- $audio.replaceWith(html);
80
-
81
- // 初始化播放器功能
82
- initAudioPlayer(playerId);
83
- });
84
- } catch (e) {
85
- console.error("音频渲染错误:", e);
86
- }
87
- }
88
-
89
- /**
90
- * 初始化音频播放器功能
91
- * @param {string} playerId - 播放器ID
92
- */
93
- function initAudioPlayer(playerId) {
94
- const $player = $(`#${playerId}`);
95
- const audio = $player.find("audio")[0];
96
- const $playBtn = $player.find(".u-play");
97
- const $needle = $player.find(".u-needle");
98
- const $avatar = $player.find(".u-avatar");
99
- const $progressBar = $player.find(".u-progress-bar");
100
- const $progressFill = $player.find(".u-progress-fill");
101
- const $progressHandle = $player.find(".u-progress-handle");
102
-
103
- if (!audio) {
104
- console.warn("音频元素未找到", playerId);
105
- return;
106
- }
107
-
108
- let isPlaying = false;
109
- let isDragging = false;
110
-
111
- // 播放/暂停切换
112
- $playBtn.on("click", function () {
113
- const playIcon = $(this).data("play-icon");
114
- const stopIcon = $(this).data("stop-icon");
115
-
116
- if (isPlaying) {
117
- audio.pause();
118
- $(this).attr("src", playIcon);
119
- $needle.removeClass("isPlaying");
120
- $avatar.addClass("isPaused");
121
- $player.removeClass("play");
122
- } else {
123
- audio.play();
124
- $(this).attr("src", stopIcon);
125
- $needle.addClass("isPlaying");
126
- $avatar.removeClass("isPaused").addClass("isRotate");
127
- $player.addClass("play");
128
- }
129
- isPlaying = !isPlaying;
130
- });
131
-
132
- // 音频播放状态改变
133
- audio.addEventListener("play", function () {
134
- isPlaying = true;
135
- $playBtn.attr("src", $playBtn.data("stop-icon"));
136
- $needle.addClass("isPlaying");
137
- $avatar.removeClass("isPaused").addClass("isRotate");
138
- $player.addClass("play");
139
- });
140
-
141
- audio.addEventListener("pause", function () {
142
- isPlaying = false;
143
- $playBtn.attr("src", $playBtn.data("play-icon"));
144
- $needle.removeClass("isPlaying");
145
- $avatar.addClass("isPaused");
146
- $player.removeClass("play");
147
- });
148
-
149
- // 更新进度条
150
- audio.addEventListener("timeupdate", function () {
151
- if (!isDragging && audio.duration) {
152
- const progress = (audio.currentTime / audio.duration) * 100;
153
- $progressFill.css("width", progress + "%");
154
- $progressHandle.css("left", progress + "%");
155
- }
156
- });
157
-
158
- // 进度条拖拽和点击
159
- function updateProgress(e) {
160
- const rect = $progressBar[0].getBoundingClientRect();
161
- const offsetX = e.clientX - rect.left;
162
- const progress = Math.max(0, Math.min(1, offsetX / rect.width));
163
- const newTime = progress * audio.duration;
164
-
165
- $progressFill.css("width", progress * 100 + "%");
166
- $progressHandle.css("left", progress * 100 + "%");
167
-
168
- if (isDragging || e.type === "click") {
169
- audio.currentTime = newTime;
170
- }
171
- }
172
-
173
- $progressBar.on("mousedown", function (e) {
174
- isDragging = true;
175
- updateProgress(e);
176
- });
177
-
178
- $(document).on("mousemove", function (e) {
179
- if (isDragging) {
180
- updateProgress(e);
181
- }
182
- });
183
-
184
- $(document).on("mouseup", function (e) {
185
- if (isDragging) {
186
- updateProgress(e);
187
- isDragging = false;
188
- }
189
- });
190
-
191
- $progressBar.on("click", function (e) {
192
- updateProgress(e);
193
- });
194
-
195
- // 音频结束
196
- audio.addEventListener("ended", function () {
197
- isPlaying = false;
198
- audio.currentTime = 0;
199
- $playBtn.attr("src", $playBtn.data("play-icon"));
200
- $needle.removeClass("isPlaying");
201
- $avatar.removeClass("isRotate isPaused");
202
- $player.removeClass("play");
203
- $progressFill.css("width", "0%");
204
- $progressHandle.css("left", "0%");
205
- });
206
-
207
- // 检查标题是否需要滚动
208
- checkTextWidth($player);
209
- }
210
-
211
- /**
212
- * 检查文本宽度,决定是否需要滚动动画
213
- * @param {jQuery} $player - 播放器元素
214
- */
215
- function checkTextWidth($player) {
216
- const $wrapper = $player.find(".marquee-wrapper");
217
- const $text = $player.find(".marquee-text");
218
-
219
- if (!$text.length || !$wrapper.length) return;
220
-
221
- const textWidth = $text[0].offsetWidth;
222
- const containerWidth = $wrapper.parent()[0].clientWidth;
223
-
224
- if (textWidth > containerWidth) {
225
- $wrapper.addClass("marquee-active");
226
- // 添加复制的文本用于无缝滚动
227
- const copyText = $text.clone().addClass("copy");
228
- $wrapper.append(copyText);
229
-
230
- // 计算动画时长
231
- const totalWidth = textWidth * 2 + 180; // 180px 是间距
232
- const duration = totalWidth / 30; // 30px/s
233
- $wrapper.css("animation-duration", `${duration}s`);
234
- $wrapper.addClass("marquee-animate");
235
- }
236
- }
237
-
238
- export default renderVoice;
package/assets/js/xss.js DELETED
@@ -1,130 +0,0 @@
1
- import sanitizeHtml from "sanitize-html";
2
- import * as cheerio from "cheerio";
3
- import postcss from "postcss";
4
- import safeParser from "postcss-safe-parser";
5
-
6
- const FORBID = new Set(["script", "object", "embed", "applet", "base", "meta", "link"]);
7
-
8
- const EXTRA_TAGS = [
9
- "img",
10
- "h1", "h2", "h3", "h4", "h5", "h6",
11
- "table", "thead", "tbody", "tr", "th", "td",
12
- "blockquote", "pre", "code", "hr",
13
- "video", "source",
14
- "iframe", "style",
15
- "colgroup", "col",
16
- ];
17
-
18
- // 必须顶层的 at-rule(你说不需要动画,但 keyframes 也可能被编辑器/作者写进来,留着更稳)
19
- const TOP_LEVEL_AT = new Set(["keyframes", "-webkit-keyframes", "font-face"]);
20
-
21
- function stripDangerousCss(css) {
22
- if (!css) return css;
23
- return css
24
- .replace(/@import\s+[^;]+;?/gi, "")
25
- .replace(/url\s*\(\s*[^)]+\s*\)/gi, "")
26
- .replace(/expression\s*\([^)]*\)/gi, "");
27
- }
28
-
29
- // 暴力把 style 内容 nest 到 .c-article
30
- function nestCssBrutally(cssText, scope = ".c-article") {
31
- let css = stripDangerousCss(cssText || "");
32
- if (!css.trim()) return css;
33
-
34
- const root = postcss.parse(css, { parser: safeParser });
35
-
36
- const top = [];
37
- const rest = [];
38
-
39
- (root.nodes || []).forEach((node) => {
40
- if (node.type === "atrule" && TOP_LEVEL_AT.has(String(node.name).toLowerCase())) {
41
- top.push(node.toString());
42
- } else {
43
- rest.push(node.toString());
44
- }
45
- });
46
-
47
- const restCss = rest.join("\n").trim();
48
- if (!restCss) return top.join("\n").trim();
49
-
50
- return `${top.join("\n")}\n${scope}{\n${restCss}\n}`.trim();
51
- }
52
-
53
- function nestAllStyleTags(html, scope = ".c-article") {
54
- const $ = cheerio.load(html, { decodeEntities: false });
55
-
56
- $("style").each((_, el) => {
57
- const oldCss = $(el).html() || "";
58
- const newCss = nestCssBrutally(oldCss, scope);
59
- $(el).text(newCss);
60
- });
61
-
62
- return $.html();
63
- }
64
-
65
- export default function sanitizeRichText(html) {
66
- if (!html) return html;
67
-
68
- // 先把 <style> 内容暴力 nest 到 .c-article
69
- html = nestAllStyleTags(html, ".c-article");
70
-
71
- const allowedTags = sanitizeHtml.defaults.allowedTags
72
- .concat(EXTRA_TAGS)
73
- .filter((t, i, arr) => arr.indexOf(t) === i)
74
- .filter((t) => !FORBID.has(t));
75
-
76
- return sanitizeHtml(html, {
77
- disallowedTagsMode: "discard",
78
- allowedTags,
79
-
80
- allowedAttributes: {
81
- "*": ["class", "style", "title", "id", "data-*"],
82
- a: ["href", "target", "rel", "title", "class", "style"],
83
- img: ["src", "alt", "title", "width", "height", "class", "style", "loading", "decoding"],
84
- video: ["controls", "width", "height", "class", "style"],
85
- source: ["src", "type"],
86
- iframe: ["src", "width", "height", "frameborder", "scrolling", "allowfullscreen", "sandbox", "referrerpolicy", "class", "style"],
87
- td: ["colspan", "rowspan", "align", "valign", "class", "style"],
88
- th: ["colspan", "rowspan", "align", "valign", "class", "style"],
89
- col: ["span", "width", "class", "style"],
90
- style: ["type", "media"],
91
- },
92
-
93
- allowedSchemes: ["http", "https", "mailto", "tel"],
94
- allowProtocolRelative: true,
95
- allowedSchemesByTag: { img: ["http", "https", "data"], iframe: ["http", "https"] },
96
-
97
- transformTags: {
98
- "*": (tagName, attribs) => {
99
- const out = { ...attribs };
100
-
101
- // 移除 on*
102
- for (const k of Object.keys(out)) if (/^on/i.test(k)) delete out[k];
103
-
104
- // style 属性:禁 @import / url(
105
- if (typeof out.style === "string" && out.style) {
106
- let s = out.style;
107
- s = s.replace(/@import\s+[^;]+;?/gi, "");
108
- s = s.replace(/url\s*\(\s*[^)]+\s*\)/gi, "");
109
- s = s.replace(/;;+/g, ";").trim();
110
- if (!s) delete out.style;
111
- else out.style = s;
112
- }
113
-
114
- // 兜底禁 javascript:
115
- for (const key of ["href", "src"]) {
116
- if (out[key] && /^\s*javascript:/i.test(out[key])) out[key] = "";
117
- }
118
-
119
- // 仅允许 data:image/*
120
- if (tagName === "img" && typeof out.src === "string" && out.src.startsWith("data:")) {
121
- if (!/^data:image\/(png|jpe?g|gif|webp|avif|bmp|svg\+xml);/i.test(out.src)) {
122
- out.src = "";
123
- }
124
- }
125
-
126
- return { tagName, attribs: out };
127
- },
128
- },
129
- });
130
- }
@@ -1,230 +0,0 @@
1
- <template>
2
- <div class="c-article-tinymce c-article-box">
3
- <!-- <div id="c-article-origin" class="c-article-origin" ref="origin"><slot></slot></div> -->
4
- <div id="c-article" class="c-article" ref="article" v-if="pageable">
5
- <div
6
- class="c-article-chunk"
7
- v-for="(text, i) in data"
8
- :key="i"
9
- v-html="text"
10
- :class="{ on: i == page - 1 || all == true }"
11
- :id="'c-article-part' + ~~(i + 1)"
12
- ></div>
13
- </div>
14
- <div id="c-article" class="c-article" ref="article" v-else-if="data && data.length" v-html="data[0]"></div>
15
- <el-button class="c-article-all" type="primary" v-if="!all && hasPages" @click="showAll">加载全部</el-button>
16
- <el-pagination
17
- class="c-article-pages"
18
- v-if="!all"
19
- background
20
- center
21
- :page-size="1"
22
- :hide-on-single-page="true"
23
- @current-change="changePage"
24
- :current-page="page"
25
- layout="total, prev, pager, next, jumper"
26
- :total="total"
27
- ></el-pagination>
28
-
29
- <div class="w-jx3-element-pop" :style="jx3_element.style">
30
- <jx3-item :item_id="item.id" :jx3ClientType="item.client" v-show="jx3_element.type == 'item'" />
31
- <jx3-buff :client="buff.client" :id="buff.id" :level="buff.level" v-show="jx3_element.type == 'buff'" />
32
- <jx3-skill :client="skill.client" :id="skill.id" :level="skill.level" v-show="jx3_element.type == 'skill'" />
33
- <jx3-npc :client="npc.client" :id="npc.id" v-show="jx3_element.type === 'npc'" />
34
- <jx3-author :uid="author.id" v-show="jx3_element.type === 'author'" />
35
- <jx3-emotion-author :id="emotion.id" v-show="jx3_element.type === 'emotion'" />
36
- </div>
37
- </div>
38
- </template>
39
-
40
- <script>
41
- import { ElPagination, ElButton } from "element-plus";
42
- import "element-plus/dist/index.css";
43
-
44
- // XSS
45
- import execFilterXSS from "../../assets/js/script";
46
- const xss_options = {
47
- allowCommentTag: true,
48
- };
49
-
50
- // 基本文本
51
- import execLazyload from "../../assets/js/img";
52
- import execFilterIframe from "../../assets/js/iframe";
53
- import execFilterLink from "../../assets/js/a";
54
- import execSplitPages from "../../assets/js/nextpage";
55
-
56
- // 扩展文本
57
- import renderFoldBlock from "../../assets/js/fold";
58
- import renderDirectory from "../../assets/js/directory";
59
- import {renderKatexAll} from "../../assets/js/katex";
60
- import renderCode from "../../assets/js/code";
61
- import renderImgPreview from "../../assets/js/renderImgPreview";
62
- import renderMacro from "../../assets/js/macro";
63
- import renderTalent from "../../assets/js/qixue";
64
- import renderTalent2 from "../../assets/js/talent2";
65
- import renderPzIframe from "../../assets/js/pz_iframe";
66
- import renderCombo from "../../assets/js/combo";
67
- import renderJx3Element from "../../assets/js/jx3_element";
68
-
69
- // 剑三
70
- import Item from "./components/Item.vue";
71
- import Buff from "./components/Buff.vue";
72
- import Skill from "./components/Skill.vue";
73
- import Npc from "./components/Npc.vue";
74
- import Author from "./components/Author.vue";
75
- import PostAuthor from "./components/PostAuthor.vue";
76
-
77
- export default {
78
- name: "ArticleRender",
79
- props: {
80
- content: String,
81
- directorybox: String,
82
- pageable: {
83
- type: Boolean,
84
- default: true,
85
- },
86
- },
87
- data: function () {
88
- return {
89
- // 作品
90
- all: false,
91
- page: 1,
92
- data: [],
93
- mode: "",
94
- jx3_element: {
95
- style: {},
96
- type: "",
97
- },
98
- item: {},
99
- buff: {},
100
- skill: {},
101
- npc: {},
102
- author: {},
103
- emotion: {},
104
- };
105
- },
106
- computed: {
107
- total: function () {
108
- return this.chunks.length;
109
- },
110
- hasPages: function () {
111
- return this.chunks.length > 1;
112
- },
113
- origin: function () {
114
- return this.content;
115
- },
116
- chunks: function () {
117
- return this.pageable ? execSplitPages(this.origin) : [this.origin];
118
- },
119
- },
120
- methods: {
121
- doReg: function (data) {
122
- if (data) {
123
- // 过滤内容
124
- data = execLazyload(data);
125
- data = execFilterIframe(data);
126
- data = execFilterXSS(data, xss_options);
127
- data = execFilterLink(data);
128
- return data;
129
- } else {
130
- return "";
131
- }
132
- },
133
- doDOM: function ($root) {
134
- // 折叠块
135
- renderFoldBlock($root);
136
- // 代码
137
- renderCode(`code[class=^'language-']`);
138
- // Tatex
139
- renderKatexAll();
140
-
141
- // 画廊(需要在宏、奇穴、物品等之前渲染以排除下方自动生成图片)
142
- renderImgPreview();
143
- // 宏
144
- renderMacro();
145
- // 奇穴
146
- renderTalent();
147
- renderTalent2();
148
- // 物品
149
- renderJx3Element(this);
150
- // 配装
151
- renderPzIframe();
152
- // 连招
153
- renderCombo();
154
- },
155
- doDir: function () {
156
- // 显示局部
157
- let target = "";
158
- if (this.hasPages && !this.all) {
159
- target = "#c-article-part" + this.page;
160
- // 全部
161
- } else {
162
- target = "#c-article";
163
- }
164
- let dir = renderDirectory(target, this.directorybox);
165
- this.$emit("directoryRendered", dir);
166
- },
167
- changePage: function (i) {
168
- this.page = i;
169
- window.scrollTo(0, 0);
170
- this.$nextTick(() => {
171
- this.doDir();
172
- });
173
- },
174
- showAll: function () {
175
- this.all = true;
176
- this.$nextTick(() => {
177
- this.doDir();
178
- });
179
- },
180
- render: function () {
181
- let result = [];
182
- for (let chunk of this.chunks) {
183
- let _chunk = this.doReg(chunk);
184
- result.push(_chunk);
185
- }
186
- this.data = result;
187
- },
188
- run: function () {
189
- this.render();
190
-
191
- // 等待html加载完毕后
192
- this.$nextTick(() => {
193
- this.$emit("contentLoaded");
194
-
195
- // 统一DOM处理
196
- const $root = this.$refs.article;
197
- this.doDOM($root);
198
- this.$emit("contentRendered");
199
-
200
- // 目录处理
201
- this.doDir();
202
- });
203
- },
204
- },
205
- watch: {
206
- content: function () {
207
- this.run();
208
- },
209
- },
210
- mounted: function () {
211
- const params = new URLSearchParams(location.search);
212
- this.mode = params.get("mode") || "";
213
- this.run();
214
- },
215
- components: {
216
- "el-pagination": ElPagination,
217
- "el-button": ElButton,
218
- "jx3-item": Item,
219
- "jx3-buff": Buff,
220
- "jx3-skill": Skill,
221
- "jx3-npc": Npc,
222
- "jx3-author": Author,
223
- "jx3-emotion-author": PostAuthor,
224
- },
225
- };
226
- </script>
227
-
228
- <style lang="less">
229
- @import "../../assets/css/editor/article.less";
230
- </style>