artalk 2.0.4 → 2.1.1

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 (91) hide show
  1. package/README.md +7 -60
  2. package/dist/Artalk.css +1 -1
  3. package/dist/Artalk.es.js +5996 -1
  4. package/dist/Artalk.iife.js +266 -1
  5. package/dist/Artalk.js +266 -1
  6. package/package.json +3 -4
  7. package/src/api/index.ts +269 -0
  8. package/src/api/request.ts +125 -0
  9. package/src/artalk.ts +153 -0
  10. package/src/components/_component.ts.txt +10 -0
  11. package/src/components/action-btn.ts +169 -0
  12. package/src/components/admin/page-list.ts +194 -0
  13. package/src/components/admin/site-list-floater.ts +98 -0
  14. package/src/components/admin/site-list.ts +289 -0
  15. package/src/components/comment.ts +478 -0
  16. package/src/components/dialog.ts +63 -0
  17. package/src/components/editor-plugs/{_ExamplePlug.ts.txt → _example-plug.ts.txt} +0 -0
  18. package/src/components/editor-plugs/{EditorPlug.ts → editor-plug.ts} +3 -3
  19. package/src/components/editor-plugs/{EmoticonsPlug.less → emoticons-plug.less} +0 -0
  20. package/src/components/editor-plugs/{EmoticonsPlug.ts → emoticons-plug.ts} +9 -9
  21. package/src/components/editor-plugs/{PreviewPlug.less → preview-plug.less} +0 -0
  22. package/src/components/editor-plugs/{PreviewPlug.ts → preview-plug.ts} +10 -10
  23. package/src/components/{Editor.ts → editor.ts} +104 -102
  24. package/src/components/html/comment.html +2 -6
  25. package/src/components/html/list.html +0 -1
  26. package/src/components/html/sidebar.html +18 -4
  27. package/src/components/item-text-editor.ts +136 -0
  28. package/src/components/layer.ts +135 -0
  29. package/src/components/list-lite.ts +507 -0
  30. package/src/components/list.ts +172 -0
  31. package/src/components/pagination.ts +150 -0
  32. package/src/components/read-more-btn.ts +71 -0
  33. package/src/components/sidebar-view.ts +26 -0
  34. package/src/components/sidebar-views/comments-view.ts +66 -0
  35. package/src/components/sidebar-views/pages-view.ts +61 -0
  36. package/src/components/sidebar-views/setting-view.ts +21 -0
  37. package/src/components/sidebar-views/sites-view.ts +42 -0
  38. package/src/components/sidebar-views/transfer-view.ts +218 -0
  39. package/src/components/sidebar.ts +288 -0
  40. package/src/context.ts +42 -0
  41. package/src/default-conf.ts +42 -0
  42. package/src/lib/checker/admin-checker.ts +33 -0
  43. package/src/lib/checker/captcha-checker.ts +38 -0
  44. package/src/lib/checker/index.ts +134 -0
  45. package/src/lib/component.ts +3 -3
  46. package/src/lib/md5.js +6 -0
  47. package/src/lib/ui.ts +64 -74
  48. package/src/lib/user.ts +2 -8
  49. package/src/lib/utils.ts +30 -11
  50. package/src/main.ts +1 -1
  51. package/src/style/_color.less +8 -2
  52. package/src/style/_extend.less +81 -0
  53. package/src/style/_variables.less +3 -0
  54. package/src/style/comment.less +60 -24
  55. package/src/style/editor.less +3 -0
  56. package/src/style/item-text-editor.less +35 -0
  57. package/src/style/list.less +51 -32
  58. package/src/style/main.less +75 -1
  59. package/src/style/page-list.less +50 -0
  60. package/src/style/pagination.less +58 -0
  61. package/src/style/sidebar.less +274 -199
  62. package/src/style/site-list.less +208 -0
  63. package/tsconfig.json +27 -0
  64. package/types/artalk-config.d.ts +68 -51
  65. package/types/artalk-data.d.ts +38 -1
  66. package/types/event.d.ts +73 -0
  67. package/types/index.d.ts +1 -6
  68. package/.github/workflows/npm.yml +0 -31
  69. package/.github/workflows/pages.yml +0 -30
  70. package/dist/src/Event.d.ts +0 -59
  71. package/dist/types/artalk-config.d.ts +0 -79
  72. package/dist/types/artalk-data.d.ts +0 -153
  73. package/dist/types/global.d.ts +0 -1
  74. package/dist/types/index.d.ts +0 -8
  75. package/dist/types/vite-env.d.ts +0 -1
  76. package/example/pjax/res1.html +0 -41
  77. package/example/pjax/res2.html +0 -41
  78. package/src/Artalk.ts +0 -160
  79. package/src/Constant.ts +0 -6
  80. package/src/Context.ts +0 -34
  81. package/src/Event.d.ts +0 -59
  82. package/src/components/Comment.ts +0 -402
  83. package/src/components/Layer.ts +0 -138
  84. package/src/components/List.ts +0 -162
  85. package/src/components/ListLite.ts +0 -407
  86. package/src/components/Sidebar.ts +0 -141
  87. package/src/components/sidebar-views/AdminView.ts +0 -523
  88. package/src/components/sidebar-views/MessageView.ts +0 -48
  89. package/src/components/sidebar-views/SidebarView.ts +0 -24
  90. package/src/lib/api.ts +0 -380
  91. package/src/lib/checker.ts +0 -164
@@ -0,0 +1,135 @@
1
+ import Context from '../context'
2
+ import Component from '../lib/component'
3
+ import * as Utils from '../lib/utils'
4
+ import * as Ui from '../lib/ui'
5
+
6
+ export default class Layer extends Component {
7
+ private name: string
8
+ private $wrap: HTMLElement
9
+ private $mask: HTMLElement
10
+
11
+ private maskClickHideEnable: boolean = true
12
+
13
+ private bodyStyleOrgOverflow = ''
14
+ private bodyStyleOrgPaddingRight = ''
15
+
16
+ constructor (ctx: Context, name: string, el?: HTMLElement) {
17
+ super(ctx)
18
+
19
+ this.name = name
20
+ const { $wrap, $mask } = GetLayerWrap(ctx)
21
+ this.$wrap = $wrap
22
+ this.$mask = $mask
23
+
24
+ this.$el = this.$wrap.querySelector(`[data-layer-name="${name}"].atk-layer-item`)!
25
+ if (this.$el === null) {
26
+ // 若传递 layer 元素为空
27
+ if (!el) {
28
+ this.$el = Utils.createElement()
29
+ this.$el.classList.add('atk-layer-item')
30
+ } else {
31
+ this.$el = el
32
+ }
33
+ }
34
+ this.$el.setAttribute('data-layer-name', name)
35
+ this.$el.style.display = 'none'
36
+
37
+ // 添加到 layers wrap 中
38
+ this.$wrap.append(this.$el)
39
+ }
40
+
41
+ getName () {
42
+ return this.name
43
+ }
44
+
45
+ getWrapEl () {
46
+ return this.$wrap
47
+ }
48
+
49
+ getEl () {
50
+ return this.$el
51
+ }
52
+
53
+ private static hideTimeoutList: number[] = []
54
+
55
+ show () {
56
+ Layer.hideTimeoutList.forEach(item => {
57
+ clearTimeout(item)
58
+ })
59
+ Layer.hideTimeoutList = []
60
+
61
+ this.$wrap.style.display = 'block'
62
+ this.$mask.style.display = 'block'
63
+ this.$mask.classList.add('atk-fade-in')
64
+ this.$el.style.display = ''
65
+
66
+ this.$mask.onclick = () => {
67
+ if (this.maskClickHideEnable) this.hide()
68
+ }
69
+
70
+ // body style 禁止滚动 + 防抖
71
+ this.bodyStyleOrgOverflow = document.body.style.overflow
72
+ this.bodyStyleOrgPaddingRight = document.body.style.paddingRight
73
+ document.body.style.overflow = 'hidden'
74
+
75
+ const bpr = parseInt(window.getComputedStyle(document.body, null).getPropertyValue('padding-right'), 10)
76
+ document.body.style.paddingRight = `${Ui.getScrollBarWidth() + bpr || 0}px`
77
+ }
78
+
79
+ hide () {
80
+ Layer.hideTimeoutList.push(window.setTimeout(() => {
81
+ this.$wrap.style.display = 'none'
82
+ // body style 禁止滚动解除
83
+ document.body.style.overflow = this.bodyStyleOrgOverflow
84
+ document.body.style.paddingRight = this.bodyStyleOrgPaddingRight
85
+ }, 450))
86
+
87
+ this.$wrap.classList.add('atk-fade-out')
88
+ Layer.hideTimeoutList.push(window.setTimeout(() => {
89
+ this.$wrap.style.display = 'none'
90
+ this.$wrap.classList.remove('atk-fade-out')
91
+ }, 200))
92
+
93
+ this.$el.style.display = 'none'
94
+ }
95
+
96
+ setMaskClickHide (enable: boolean) {
97
+ this.maskClickHideEnable = enable
98
+ }
99
+
100
+ /** 销毁 - 无动画 */
101
+ disposeNow () {
102
+ document.body.style.overflow = ''
103
+ this.$el.remove()
104
+ // this.$el dispose
105
+ this.checkCleanLayer()
106
+ }
107
+
108
+ /** 销毁 */
109
+ dispose () {
110
+ this.hide()
111
+ this.$el.remove()
112
+ // this.$el dispose
113
+ this.checkCleanLayer()
114
+ }
115
+
116
+ checkCleanLayer () {
117
+ if (this.getWrapEl().querySelectorAll('.atk-layer-item').length === 0) {
118
+ this.$wrap.style.display = 'none'
119
+ }
120
+ }
121
+ }
122
+
123
+ export function GetLayerWrap (ctx: Context): { $wrap: HTMLElement, $mask: HTMLElement } {
124
+ let $wrap = document.querySelector<HTMLElement>(`.atk-layer-wrap#ctx-${ctx.cid}`)
125
+ if (!$wrap) {
126
+ $wrap = Utils.createElement(
127
+ `<div class="atk-layer-wrap" id="ctx-${ctx.cid}" style="display: none;"><div class="atk-layer-mask"></div></div>`
128
+ )
129
+ document.body.appendChild($wrap)
130
+ }
131
+
132
+ const $mask = $wrap.querySelector<HTMLElement>('.atk-layer-mask')!
133
+
134
+ return { $wrap, $mask }
135
+ }
@@ -0,0 +1,507 @@
1
+ import Context from '../context'
2
+ import Component from '../lib/component'
3
+ import * as Utils from '../lib/utils'
4
+ import * as Ui from '../lib/ui'
5
+ import Api from '../api'
6
+ import Comment from './comment'
7
+ import { ListData, CommentData, NotifyData, ApiVersionData } from '~/types/artalk-data'
8
+ import Pagination from './pagination'
9
+ import ReadMoreBtn from './read-more-btn'
10
+
11
+ export default class ListLite extends Component {
12
+ private $parent: HTMLElement
13
+
14
+ private $commentsWrap: HTMLElement
15
+ public comments: Comment[] = []
16
+
17
+ public data?: ListData
18
+ private pageSize: number = 15 // 每次请求获取量
19
+ private offset: number = 0
20
+ public type?: 'all'|'mentions'|'mine'|'pending'
21
+
22
+ public noCommentText: string
23
+ public renderComment?: (comment: Comment) => void
24
+ public paramsEditor?: (params: any) => void
25
+ public onAfterLoad?: (data: ListData) => void
26
+
27
+ private isLoading: boolean = false
28
+ public isFirstLoad = true
29
+
30
+ public flatMode?: boolean
31
+
32
+ public pageMode: 'pagination'|'read-more' = 'pagination'
33
+ public pagination?: Pagination
34
+ public readMoreBtn?: ReadMoreBtn
35
+ public autoLoadScrollEvent?: any
36
+ public autoLoadListenerAt?: HTMLElement
37
+
38
+ public unread: NotifyData[] = []
39
+ public unreadHighlight = false
40
+
41
+ constructor (ctx: Context, $parent: HTMLElement) {
42
+ super(ctx)
43
+
44
+ this.$parent = $parent
45
+ this.$el = Utils.createElement(
46
+ `<div class="atk-list-lite">
47
+ <div class="atk-list-comments-wrap"></div>
48
+ </div>`)
49
+ this.$commentsWrap = this.$el.querySelector('.atk-list-comments-wrap')!
50
+
51
+ // 查看更多
52
+ this.pageSize = this.conf.pagination ? (this.conf.pagination.pageSize || this.pageSize) : this.pageSize
53
+
54
+ this.noCommentText = this.conf.noComment || '无评论'
55
+
56
+ // 评论时间自动更新
57
+ window.setInterval(() => {
58
+ this.$el.querySelectorAll<HTMLElement>('[data-atk-comment-date]').forEach(el => {
59
+ const date = el.getAttribute('data-atk-comment-date')
60
+ el.innerText = Utils.timeAgo(new Date(Number(date)))
61
+ })
62
+ }, 30 * 1000) // 30s 更新一次
63
+
64
+ this.ctx.on('unread-update', (data) => (this.updateUnread(data.notifies)))
65
+ }
66
+
67
+ public async reqComments(offset: number = 0) {
68
+ if (offset === 0 && this.pageMode !== 'pagination') { this.clearAllComments() }
69
+
70
+ // set loading
71
+ const showLoading = () => {
72
+ this.isLoading = true
73
+ if (offset === 0) Ui.showLoading(this.$el)
74
+ else if (this.pageMode === 'read-more') this.readMoreBtn!.setLoading(true)
75
+ else if (this.pageMode === 'pagination') this.pagination!.setLoading(true)
76
+ }
77
+ const hideLoading = () => {
78
+ this.isLoading = false
79
+ if (offset === 0) Ui.hideLoading(this.$el)
80
+ else if (this.pageMode === 'read-more') this.readMoreBtn!.setLoading(false)
81
+ else if (this.pageMode === 'pagination') this.pagination!.setLoading(false)
82
+ }
83
+
84
+ showLoading()
85
+ this.ctx.trigger('comments-load')
86
+
87
+ let listData: ListData
88
+ try {
89
+ listData = await new Api(this.ctx).get(offset, this.type, this.flatMode, this.paramsEditor)
90
+ } catch (e: any) {
91
+ this.onError(e.msg || String(e))
92
+ throw e
93
+ } finally {
94
+ hideLoading()
95
+ }
96
+
97
+ // version check
98
+ if (this.ctx.conf.versionCheck) {
99
+ const needUpdate = this.apiVersionCheck(listData.api_version || {})
100
+ if (needUpdate) return
101
+ }
102
+
103
+ // load data
104
+ this.offset = offset
105
+
106
+ try {
107
+ this.onLoad(listData, offset)
108
+
109
+ if (this.onAfterLoad) {
110
+ this.onAfterLoad(listData)
111
+ }
112
+ } catch (e: any) {
113
+ this.onError(String(e))
114
+ throw e
115
+ } finally {
116
+ hideLoading()
117
+ }
118
+ }
119
+
120
+ public onLoad(data: ListData, offset: number) {
121
+ Ui.setError(this.$el, null)
122
+
123
+ if (this.pageMode === 'pagination') {
124
+ this.clearAllComments()
125
+ }
126
+
127
+ this.data = data
128
+ this.importComments(data.comments)
129
+
130
+ // onLoad 时初始化
131
+ if (this.isFirstLoad) {
132
+ this.onLoadInit()
133
+ }
134
+
135
+ if (this.pageMode === 'pagination') {
136
+ this.pagination!.update(offset, this.data?.total_parents || 0)
137
+ }
138
+ if (this.pageMode === 'read-more') {
139
+ if (this.hasMoreComments) this.readMoreBtn!.show()
140
+ else this.readMoreBtn!.hide()
141
+ }
142
+
143
+ this.ctx.trigger('unread-update', { notifies: data.unread || [] })
144
+
145
+ this.isFirstLoad = false
146
+ }
147
+
148
+ /** 初始化加载更多 */
149
+ onLoadInit() {
150
+ if (this.autoLoadScrollEvent) {
151
+ const at = this.autoLoadListenerAt || document
152
+ at.removeEventListener('scroll', this.autoLoadScrollEvent)
153
+ }
154
+
155
+ if (this.pageMode === 'read-more') {
156
+ // 阅读更多按钮
157
+ const readMoreBtn = new ReadMoreBtn({
158
+ pageSize: this.pageSize,
159
+ total: 0,
160
+ onClick: async () => {
161
+ const offset = this.offset + this.pageSize
162
+ await this.reqComments(offset)
163
+ },
164
+ })
165
+ if (this.readMoreBtn) this.readMoreBtn.$el.replaceWith(readMoreBtn.$el)
166
+ else this.$el.append(readMoreBtn.$el)
167
+ this.readMoreBtn = readMoreBtn
168
+
169
+ // 滚动到底部自动加载
170
+ if (this.conf.pagination?.autoLoad) {
171
+ this.autoLoadScrollEvent = () => {
172
+ if (this.pageMode !== 'read-more') return
173
+ if (!this.hasMoreComments) return
174
+ if (this.isLoading) return
175
+
176
+ const $target = this.$el.querySelector<HTMLElement>('.atk-list-comments-wrap > .atk-comment-wrap:nth-last-child(3)') // 获取倒数第3个评论元素
177
+ if (!$target) return
178
+ if (Ui.isVisible($target, this.autoLoadListenerAt)) {
179
+ // 加载更多
180
+ this.readMoreBtn!.click()
181
+ }
182
+ }
183
+ const at = this.autoLoadListenerAt || document
184
+ at.addEventListener('scroll', this.autoLoadScrollEvent)
185
+ }
186
+ } else if (this.pageMode === 'pagination') {
187
+ // 分页条
188
+ const pagination = new Pagination(this.parentCommentsCount, {
189
+ pageSize: this.pageSize,
190
+ onChange: async (offset) => {
191
+ await this.reqComments(offset)
192
+ // 滚动到第一个评论的位置
193
+ if (this.$parent) {
194
+ let topPos = 0
195
+ if (!this.autoLoadListenerAt && this.$parent) {
196
+ topPos = Utils.getOffset(this.$parent).top
197
+ }
198
+ const at = this.autoLoadListenerAt || window
199
+ at.scroll({
200
+ top: topPos,
201
+ left: 0,
202
+ })
203
+ }
204
+ }
205
+ })
206
+ if (this.pagination) this.pagination.$el.replaceWith(pagination.$el)
207
+ else this.$el.append(pagination.$el)
208
+ this.pagination = pagination
209
+ }
210
+ }
211
+
212
+ public onError(msg: any) {
213
+ msg = String(msg)
214
+ console.error(msg)
215
+ if (this.isFirstLoad || this.pageMode === 'pagination') {
216
+ const errEl = Utils.createElement(`<span>${msg},无法获取评论列表数据<br/></span>`)
217
+ const retryBtn = Utils.createElement('<span style="cursor:pointer;">点击重新获取</span>')
218
+ retryBtn.onclick = () => {
219
+ this.reqComments(this.offset)
220
+ }
221
+ errEl.appendChild(retryBtn)
222
+ const adminBtn = Utils.createElement('<span atk-only-admin-show> | <span style="cursor:pointer;">打开控制台</span></span>')
223
+ adminBtn.onclick = () => {
224
+ this.ctx.trigger('sidebar-show')
225
+ }
226
+ if (!this.ctx.user.data.isAdmin) {
227
+ adminBtn.classList.add('atk-hide')
228
+ }
229
+ errEl.appendChild(adminBtn)
230
+ Ui.setError(this.$el, errEl)
231
+ } else {
232
+ this.readMoreBtn?.showErr(`获取失败`)
233
+ }
234
+ }
235
+
236
+ /** 刷新界面 */
237
+ public refreshUI() {
238
+ // 评论为空界面
239
+ const noComment = this.comments.length <= 0
240
+ let noCommentEl = this.$commentsWrap.querySelector<HTMLElement>('.atk-list-no-comment')
241
+
242
+ if (noComment) {
243
+ if (!noCommentEl) {
244
+ noCommentEl = Utils.createElement('<div class="atk-list-no-comment"></div>')
245
+ this.$commentsWrap.appendChild(noCommentEl)
246
+ noCommentEl.innerHTML = this.noCommentText
247
+ }
248
+ }
249
+
250
+ if (!noComment && noCommentEl)
251
+ noCommentEl.remove()
252
+
253
+ // 仅管理员显示控制
254
+ this.ctx.trigger('check-admin-show-el')
255
+ }
256
+
257
+ /** 创建新评论 */
258
+ public createComment(data: CommentData) {
259
+ const comment = new Comment(this.ctx, data)
260
+ comment.afterRender = () => {
261
+ if (this.renderComment) this.renderComment(comment)
262
+ }
263
+ comment.onDelete = (c) => {
264
+ this.deleteComment(c)
265
+ this.refreshUI()
266
+ }
267
+
268
+ return comment
269
+ }
270
+
271
+ /** 导入评论 · 通过请求数据 */
272
+ public importComments(rawData: CommentData[]) {
273
+ // 查找并导入所有子评论
274
+ const queryImportChildren = (parentC: Comment) => {
275
+ const children = rawData.filter(o => o.rid === parentC.data.id)
276
+ if (children.length === 0) return
277
+
278
+ children.forEach((itemData: CommentData) => {
279
+ itemData.is_allow_reply = parentC.data.is_allow_reply
280
+ const childC = this.createComment(itemData)
281
+ childC.render()
282
+ parentC.putChild(childC)
283
+ queryImportChildren(childC) // 递归
284
+ })
285
+ }
286
+
287
+ // 开始处理 rawData
288
+ if (!this.flatMode) {
289
+ rawData.filter((o) => o.rid === 0).forEach((rootCommentData: CommentData) => {
290
+ if (rootCommentData.is_collapsed) rootCommentData.is_allow_reply = false
291
+ const rootComment = this.createComment(rootCommentData)
292
+ rootComment.render()
293
+ this.comments.push(rootComment) // 将评论导入 comments 总表中
294
+
295
+ this.$commentsWrap.appendChild(rootComment.getEl())
296
+ rootComment.playFadeInAnim()
297
+
298
+ queryImportChildren(rootComment)
299
+ })
300
+ } else {
301
+ // 平铺模式
302
+ rawData.forEach((commentData: CommentData) => {
303
+ this.putCommentFlatMode(commentData, rawData, 'append')
304
+ })
305
+ }
306
+
307
+ this.eachComment(this.comments, (c) => {
308
+ this.checkMoreHide(c)
309
+ })
310
+
311
+ this.refreshUI()
312
+ this.ctx.trigger('comments-loaded')
313
+ }
314
+
315
+ /** 导入评论 · 平铺模式 */
316
+ private putCommentFlatMode(commentItem: CommentData, comments: CommentData[], insertMode: 'append'|'prepend') {
317
+ if (commentItem.is_collapsed) commentItem.is_allow_reply = false
318
+ const comment = this.createComment(commentItem)
319
+ if (commentItem.rid !== 0) {
320
+ const rComment = comments.find(o => o.id === commentItem.rid)
321
+ if (rComment) comment.replyTo = rComment
322
+ }
323
+ comment.render()
324
+
325
+ // 将评论导入 comments 总表中
326
+ if (insertMode === 'append') {
327
+ this.comments.push(comment)
328
+ } else {
329
+ this.comments.unshift(comment)
330
+ }
331
+
332
+ if (commentItem.visible) {
333
+ if (insertMode === 'append') {
334
+ this.$commentsWrap.appendChild(comment.getEl())
335
+ } else {
336
+ this.$commentsWrap.prepend(comment.getEl())
337
+ }
338
+ comment.playFadeInAnim()
339
+ }
340
+
341
+ this.checkMoreHide(comment)
342
+ }
343
+
344
+ /** 插入评论 · 首部添加 */
345
+ public insertComment(commentData: CommentData) {
346
+ if (!this.flatMode) {
347
+ const comment = this.createComment(commentData)
348
+ comment.render()
349
+
350
+ if (commentData.rid !== 0) {
351
+ this.findComment(commentData.rid)?.putChild(comment)
352
+ } else {
353
+ this.$commentsWrap.prepend(comment.getEl())
354
+ this.comments.unshift(comment)
355
+ }
356
+
357
+ Ui.scrollIntoView(comment.getEl()) // 滚动到可以见
358
+ comment.playFadeInAnim() // 播放评论渐出动画
359
+
360
+ this.checkMoreHide(comment)
361
+ } else {
362
+ this.putCommentFlatMode(commentData, this.comments.map(c => c.data), 'prepend')
363
+ }
364
+
365
+ if (this.data) this.data.total += 1 // 评论数增加 1
366
+ this.refreshUI() // 更新 list 界面
367
+ this.ctx.trigger('comments-loaded')
368
+ }
369
+
370
+ checkMoreHide(c: Comment) {
371
+ const childrenH = this.ctx.conf.heightLimit?.children
372
+ const contentH = this.ctx.conf.heightLimit?.content
373
+ const isChildrenLimit = typeof childrenH === 'number' && childrenH > 0
374
+ const isContentLimit = typeof contentH === 'number' && contentH > 0
375
+
376
+ // 子评论内容过多隐藏
377
+ if (isChildrenLimit && c.getIsRoot()) {
378
+ c.checkMoreHide(c.$children, childrenH || 300)
379
+ }
380
+
381
+ // 评论内容过多隐藏
382
+ if (isContentLimit) {
383
+ c.checkMoreHide(c.$content, contentH || 200)
384
+ if (c.$replyTo) c.checkMoreHide(c.$replyTo, contentH || 200) // 平铺模式回复内容
385
+ }
386
+ }
387
+
388
+ /** 获取评论总数 (包括子评论) */
389
+ get commentsCount(): number {
390
+ return Number(this.data?.total) || 0
391
+ }
392
+
393
+ get parentCommentsCount() {
394
+ return Number(this.data?.total_parents) || 0
395
+ }
396
+
397
+ /** 是否还有更多的评论 */
398
+ get hasMoreComments(): boolean {
399
+ if (!this.data) return false
400
+ return this.data.total_parents > (this.offset + this.pageSize)
401
+ }
402
+
403
+ /** 遍历操作 Comment (包括子评论) */
404
+ public eachComment(commentList: Comment[], action: (comment: Comment, levelList: Comment[]) => boolean|void) {
405
+ if (commentList.length === 0) return
406
+ commentList.every((item) => {
407
+ if (action(item, commentList) === false) return false
408
+ this.eachComment(item.getChildren(), action)
409
+ return true
410
+ })
411
+ }
412
+
413
+ /** 查找评论项 */
414
+ public findComment(id: number, src?: Comment[]): Comment|null {
415
+ if (!src) src = this.comments
416
+ let comment: Comment|null = null
417
+ this.eachComment(src, (item) => {
418
+ if (item.data.id === id) {
419
+ comment = item
420
+ return false
421
+ }
422
+ return true
423
+ })
424
+
425
+ return comment
426
+ }
427
+
428
+ /** 获取评论总数 */
429
+ public getCommentCount(): number {
430
+ let count = 0
431
+ this.eachComment(this.comments, () => { count++ })
432
+ return count
433
+ }
434
+
435
+ /** 删除评论 */
436
+ public deleteComment(comment: number|Comment) {
437
+ let findComment: Comment|null
438
+ if (typeof comment === 'number') {
439
+ findComment = this.findComment(comment)
440
+ if (!findComment) throw Error(`未找到评论 ${comment}`)
441
+ } else findComment = comment
442
+
443
+ findComment.getEl().remove()
444
+ this.eachComment(this.comments, (item, levelList) => {
445
+ if (item === findComment) {
446
+ levelList.splice(levelList.indexOf(item), 1)
447
+ return false
448
+ }
449
+ return true
450
+ })
451
+
452
+ this.refreshUI()
453
+ }
454
+
455
+ /** 清空所有评论 */
456
+ public clearAllComments() {
457
+ this.$commentsWrap.innerHTML = ''
458
+ this.data = undefined
459
+ this.comments = []
460
+ }
461
+
462
+ public updateUnread(notifies: NotifyData[]) {
463
+ this.unread = notifies
464
+
465
+ // 高亮评论
466
+ if (this.unreadHighlight) {
467
+ this.eachComment(this.comments, (comment) => {
468
+ const notify = this.unread.find(o => o.comment_id === comment.data.id)
469
+ if (notify) {
470
+ comment.setUnread(true)
471
+ comment.setOpenURL(notify.read_link)
472
+ comment.openEvt = () => {
473
+ this.unread = this.unread.filter(o => o.comment_id !== comment.data.id) // remove
474
+ this.ctx.trigger('unread-update', {
475
+ notifies: this.unread
476
+ })
477
+ }
478
+ } else {
479
+ comment.setUnread(false)
480
+ }
481
+ })
482
+ }
483
+ }
484
+
485
+ public apiVersionCheck(versionData: ApiVersionData): boolean {
486
+ const needVersion = versionData?.fe_min_version || '0.0.0'
487
+ const needUpdate = Utils.versionCompare(needVersion, ARTALK_VERSION) === 1
488
+ if (needUpdate) {
489
+ // 需要更新
490
+ const errEl = Utils.createElement(`<div>前端 Artalk 版本已过时,请更新以获得完整体验<br/>`
491
+ + `若您是站点管理员,请前往 “<a href="https://artalk.js.org/" target="_blank">官方文档</a>” 获取帮助`
492
+ + `<br/><br/>`
493
+ + `<span style="color: var(--at-color-meta);">前端版本 ${ARTALK_VERSION},需求版本 >= ${needVersion}</span><br/><br/>`
494
+ + `</div>`)
495
+ const ignoreBtn = Utils.createElement('<span style="cursor:pointer;">忽略</span>')
496
+ ignoreBtn.onclick = () => {
497
+ Ui.setError(this.ctx, null)
498
+ this.ctx.conf.versionCheck = false
499
+ this.reqComments(0)
500
+ }
501
+ errEl.append(ignoreBtn)
502
+ Ui.setError(this.ctx, errEl, '<span class="atk-warn-title">Artalk Warn</span>')
503
+ }
504
+
505
+ return needUpdate
506
+ }
507
+ }