artalk 2.2.8 → 2.2.9
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.
- package/dist/Artalk.css +1 -1
- package/dist/Artalk.es.js +212 -116
- package/dist/Artalk.iife.js +1 -1
- package/dist/Artalk.js +1 -1
- package/package.json +1 -1
- package/src/artalk.ts +1 -0
- package/src/components/comment.ts +53 -21
- package/src/components/editor.ts +15 -2
- package/src/components/list-lite.ts +60 -77
- package/src/components/list-nest.ts +66 -0
- package/src/components/list.ts +36 -16
- package/src/defaults.ts +2 -1
- package/src/style/comment.less +21 -0
- package/types/artalk-config.d.ts +2 -2
package/package.json
CHANGED
package/src/artalk.ts
CHANGED
|
@@ -15,6 +15,7 @@ export default class Comment extends Component {
|
|
|
15
15
|
|
|
16
16
|
public $main!: HTMLElement
|
|
17
17
|
public $header!: HTMLElement
|
|
18
|
+
public $headerNick!: HTMLElement
|
|
18
19
|
public $body!: HTMLElement
|
|
19
20
|
public $content!: HTMLElement
|
|
20
21
|
public $children!: HTMLElement|null
|
|
@@ -23,12 +24,15 @@ export default class Comment extends Component {
|
|
|
23
24
|
public voteBtnDown?: ActionBtn
|
|
24
25
|
|
|
25
26
|
public parent: Comment|null
|
|
26
|
-
public
|
|
27
|
-
private
|
|
28
|
-
|
|
27
|
+
public nestCurt: number
|
|
28
|
+
private nestMax: number // 最大嵌套层数
|
|
29
|
+
private children: Comment[] = []
|
|
29
30
|
|
|
30
|
-
public
|
|
31
|
-
public
|
|
31
|
+
public flatMode: boolean = false
|
|
32
|
+
public replyTo?: CommentData // 回复对象
|
|
33
|
+
|
|
34
|
+
public $replyTo?: HTMLElement // 回复评论内容 (平铺下显示)
|
|
35
|
+
public $replyAt?: HTMLElement // 回复 AT(层级嵌套下显示)
|
|
32
36
|
|
|
33
37
|
public afterRender?: () => void
|
|
34
38
|
|
|
@@ -43,13 +47,13 @@ export default class Comment extends Component {
|
|
|
43
47
|
super(ctx)
|
|
44
48
|
|
|
45
49
|
// 最大嵌套数
|
|
46
|
-
this.
|
|
50
|
+
this.nestMax = ctx.conf.nestMax || 3
|
|
47
51
|
|
|
48
52
|
this.data = { ...data }
|
|
49
53
|
this.data.date = this.data.date.replace(/-/g, '/') // 解决 Safari 日期解析 NaN 问题
|
|
50
54
|
|
|
51
55
|
this.parent = null
|
|
52
|
-
this.
|
|
56
|
+
this.nestCurt = 1 // 现在已嵌套 n 层
|
|
53
57
|
}
|
|
54
58
|
|
|
55
59
|
/** 渲染 UI */
|
|
@@ -70,6 +74,7 @@ export default class Comment extends Component {
|
|
|
70
74
|
this.renderAvatar()
|
|
71
75
|
this.renderHeader()
|
|
72
76
|
this.renderContent()
|
|
77
|
+
this.renderReplyAt()
|
|
73
78
|
this.renderReplyTo()
|
|
74
79
|
this.renderPending()
|
|
75
80
|
this.renderActionBtn()
|
|
@@ -117,14 +122,14 @@ export default class Comment extends Component {
|
|
|
117
122
|
}
|
|
118
123
|
|
|
119
124
|
private renderHeader() {
|
|
120
|
-
|
|
125
|
+
this.$headerNick = this.$el.querySelector<HTMLElement>('.atk-nick')!
|
|
121
126
|
if (this.data.link) {
|
|
122
127
|
const $nickA = Utils.createElement<HTMLLinkElement>('<a target="_blank" rel="noreferrer noopener nofollow"></a>')
|
|
123
128
|
$nickA.innerText = this.data.nick
|
|
124
129
|
$nickA.href = this.data.link
|
|
125
|
-
|
|
130
|
+
this.$headerNick.append($nickA)
|
|
126
131
|
} else {
|
|
127
|
-
|
|
132
|
+
this.$headerNick.innerText = this.data.nick
|
|
128
133
|
}
|
|
129
134
|
|
|
130
135
|
const $badge = this.$el.querySelector<HTMLElement>('.atk-badge')!
|
|
@@ -138,7 +143,7 @@ export default class Comment extends Component {
|
|
|
138
143
|
|
|
139
144
|
if (this.data.is_pinned) {
|
|
140
145
|
const $pinnedBadge = Utils.createElement(`<span class="atk-item atk-pinned-badge">置顶</span>`) // 置顶徽章
|
|
141
|
-
|
|
146
|
+
this.$headerNick.insertAdjacentElement('afterend', $pinnedBadge)
|
|
142
147
|
}
|
|
143
148
|
|
|
144
149
|
const $date = this.$el.querySelector<HTMLElement>('.atk-date')!
|
|
@@ -189,8 +194,22 @@ export default class Comment extends Component {
|
|
|
189
194
|
})
|
|
190
195
|
}
|
|
191
196
|
|
|
197
|
+
// 层级嵌套模式显示 At
|
|
198
|
+
private renderReplyAt() {
|
|
199
|
+
if (this.flatMode || this.data.rid === 0) return // not 平铺模式 或 根评论
|
|
200
|
+
if (this.$replyAt) return // not 关闭显示 或 已经显示
|
|
201
|
+
if (!this.replyTo) return
|
|
202
|
+
|
|
203
|
+
this.$replyAt = Utils.createElement(`<span class="atk-item atk-reply-at"><span class="atk-arrow"></span><span class="atk-nick"></span></span>`)
|
|
204
|
+
this.$replyAt.querySelector<HTMLElement>('.atk-nick')!.innerText = `${this.replyTo.nick}`
|
|
205
|
+
this.$replyAt.onclick = () => { this.goToReplyComment() }
|
|
206
|
+
|
|
207
|
+
this.$headerNick.insertAdjacentElement('afterend', this.$replyAt)
|
|
208
|
+
}
|
|
209
|
+
|
|
192
210
|
// 回复的对象
|
|
193
211
|
private renderReplyTo() {
|
|
212
|
+
if (!this.flatMode) return // 仅平铺模式显示
|
|
194
213
|
if (!this.replyTo) return
|
|
195
214
|
|
|
196
215
|
this.$replyTo = Utils.createElement(`
|
|
@@ -198,13 +217,23 @@ export default class Comment extends Component {
|
|
|
198
217
|
<div class="atk-meta">回复 <span class="atk-nick"></span>:</div>
|
|
199
218
|
<div class="atk-content"></div>
|
|
200
219
|
</div>`)
|
|
201
|
-
this.$replyTo.querySelector<HTMLElement>('.atk-nick')
|
|
220
|
+
const $nick = this.$replyTo.querySelector<HTMLElement>('.atk-nick')!
|
|
221
|
+
$nick.innerText = `@${this.replyTo.nick}`
|
|
222
|
+
$nick.onclick = () => { this.goToReplyComment() }
|
|
202
223
|
let replyContent = Utils.marked(this.ctx, this.replyTo.content)
|
|
203
224
|
if (this.replyTo.is_collapsed) replyContent = '[已折叠]'
|
|
204
225
|
this.$replyTo.querySelector<HTMLElement>('.atk-content')!.innerHTML = replyContent
|
|
205
226
|
this.$body.prepend(this.$replyTo)
|
|
206
227
|
}
|
|
207
228
|
|
|
229
|
+
public goToReplyComment() {
|
|
230
|
+
const origHash = window.location.hash
|
|
231
|
+
const modifyHash = `#atk-comment-${this.data.rid}`
|
|
232
|
+
|
|
233
|
+
window.location.hash = modifyHash
|
|
234
|
+
if (modifyHash === origHash) window.dispatchEvent(new Event('hashchange')) // 强制触发事件
|
|
235
|
+
}
|
|
236
|
+
|
|
208
237
|
// 待审核状态
|
|
209
238
|
private renderPending() {
|
|
210
239
|
if (!this.data.is_pending) return
|
|
@@ -324,30 +353,33 @@ export default class Comment extends Component {
|
|
|
324
353
|
return this.children
|
|
325
354
|
}
|
|
326
355
|
|
|
327
|
-
putChild(childC: Comment) {
|
|
356
|
+
putChild(childC: Comment, insertMode: 'append'|'prepend' = 'append') {
|
|
328
357
|
childC.parent = this
|
|
329
|
-
childC.
|
|
358
|
+
childC.nestCurt = this.nestCurt + 1 // 嵌套层数 +1
|
|
330
359
|
this.children.push(childC)
|
|
331
360
|
|
|
332
|
-
this.getChildrenEl()
|
|
361
|
+
const $children = this.getChildrenEl()
|
|
362
|
+
if (insertMode === 'append') $children.append(childC.getEl())
|
|
363
|
+
else if (insertMode === 'prepend') $children.prepend(childC.getEl())
|
|
364
|
+
|
|
333
365
|
childC.playFadeInAnim()
|
|
334
366
|
|
|
335
367
|
// 内容限高
|
|
336
368
|
childC.checkHeightLimitArea('content')
|
|
337
369
|
}
|
|
338
370
|
|
|
339
|
-
getChildrenEl() {
|
|
340
|
-
if (this.$children
|
|
341
|
-
// console.log(this.
|
|
342
|
-
if (this.
|
|
371
|
+
getChildrenEl(): HTMLElement {
|
|
372
|
+
if (!this.$children) {
|
|
373
|
+
// console.log(this.nestCurt)
|
|
374
|
+
if (this.nestCurt < this.nestMax) {
|
|
343
375
|
this.$children = Utils.createElement('<div class="atk-comment-children"></div>')
|
|
344
|
-
this.$main.
|
|
376
|
+
this.$main.append(this.$children)
|
|
345
377
|
} else if (this.parent) {
|
|
346
378
|
this.$children = this.parent.getChildrenEl()
|
|
347
379
|
}
|
|
348
380
|
}
|
|
349
381
|
|
|
350
|
-
return this.$children
|
|
382
|
+
return this.$children!
|
|
351
383
|
}
|
|
352
384
|
|
|
353
385
|
getParent() {
|
package/src/components/editor.ts
CHANGED
|
@@ -85,9 +85,22 @@ export default class Editor extends Component {
|
|
|
85
85
|
if (inputEl && inputEl instanceof HTMLInputElement) {
|
|
86
86
|
inputEl.value = this.user.data[field] || ''
|
|
87
87
|
// 绑定事件
|
|
88
|
-
inputEl.addEventListener('input', () => this.
|
|
88
|
+
inputEl.addEventListener('input', () => this.onHeaderInput(field, inputEl))
|
|
89
89
|
}
|
|
90
90
|
})
|
|
91
|
+
|
|
92
|
+
// Link URL 自动补全协议
|
|
93
|
+
const $linkInput = this.getInputEl('link')
|
|
94
|
+
if ($linkInput) {
|
|
95
|
+
$linkInput.addEventListener('change', () => {
|
|
96
|
+
const link = $linkInput.value.trim()
|
|
97
|
+
if (!!link && !/^(http|https):\/\//.test(link)) {
|
|
98
|
+
$linkInput.value = `https://${link}`
|
|
99
|
+
this.user.data.link = $linkInput.value
|
|
100
|
+
this.saveUser()
|
|
101
|
+
}
|
|
102
|
+
})
|
|
103
|
+
}
|
|
91
104
|
}
|
|
92
105
|
|
|
93
106
|
getInputEl (field: string) {
|
|
@@ -101,7 +114,7 @@ export default class Editor extends Component {
|
|
|
101
114
|
}
|
|
102
115
|
|
|
103
116
|
/** header 输入框内容变化事件 */
|
|
104
|
-
|
|
117
|
+
onHeaderInput(field: string, inputEl: HTMLInputElement) {
|
|
105
118
|
this.user.data[field] = inputEl.value.trim()
|
|
106
119
|
|
|
107
120
|
// 若修改的是 nick or email
|
|
@@ -7,11 +7,14 @@ import Api from '../api'
|
|
|
7
7
|
import Comment from './comment'
|
|
8
8
|
import Pagination from './pagination'
|
|
9
9
|
import ReadMoreBtn from './read-more-btn'
|
|
10
|
+
import * as ListNest from './list-nest'
|
|
10
11
|
import { backendMinVersion } from '../../package.json'
|
|
11
12
|
|
|
12
13
|
export default class ListLite extends Component {
|
|
13
14
|
protected $commentsWrap: HTMLElement
|
|
14
|
-
|
|
15
|
+
|
|
16
|
+
protected commentList: Comment[] = [] // Note: 无层级结构 + 无须排列
|
|
17
|
+
protected get commentDataList() { return this.commentList.map(c => c.data) }
|
|
15
18
|
|
|
16
19
|
protected data?: ListData
|
|
17
20
|
protected isLoading: boolean = false
|
|
@@ -21,6 +24,9 @@ export default class ListLite extends Component {
|
|
|
21
24
|
/** 平铺模式 */
|
|
22
25
|
public flatMode = false
|
|
23
26
|
|
|
27
|
+
/** 嵌套模式 */
|
|
28
|
+
public nestSortBy: ListNest.SortByType = 'DATE_DESC'
|
|
29
|
+
|
|
24
30
|
/** 分页 */
|
|
25
31
|
public pageMode: 'pagination'|'read-more' = 'pagination'
|
|
26
32
|
public pageSize: number = 20 // 每次请求获取量
|
|
@@ -265,7 +271,7 @@ export default class ListLite extends Component {
|
|
|
265
271
|
/** 刷新界面 */
|
|
266
272
|
public refreshUI() {
|
|
267
273
|
// 无评论
|
|
268
|
-
const isNoComment = this.
|
|
274
|
+
const isNoComment = this.commentList.length <= 0
|
|
269
275
|
let $noComment = this.$commentsWrap.querySelector<HTMLElement>('.atk-list-no-comment')
|
|
270
276
|
|
|
271
277
|
if (isNoComment) {
|
|
@@ -283,8 +289,11 @@ export default class ListLite extends Component {
|
|
|
283
289
|
}
|
|
284
290
|
|
|
285
291
|
/** 创建新评论 */
|
|
286
|
-
protected createComment(
|
|
287
|
-
|
|
292
|
+
protected createComment(cData: CommentData, ctxData?: CommentData[]) {
|
|
293
|
+
if (!ctxData) ctxData = this.commentDataList
|
|
294
|
+
|
|
295
|
+
const comment = new Comment(this.ctx, cData)
|
|
296
|
+
comment.flatMode = this.flatMode
|
|
288
297
|
comment.afterRender = () => {
|
|
289
298
|
if (this.renderComment) this.renderComment(comment)
|
|
290
299
|
}
|
|
@@ -293,6 +302,9 @@ export default class ListLite extends Component {
|
|
|
293
302
|
this.refreshUI()
|
|
294
303
|
}
|
|
295
304
|
|
|
305
|
+
// 子评论查找回复对象
|
|
306
|
+
comment.replyTo = (cData.rid ? ctxData.find(c => c.id === cData.rid) : undefined)
|
|
307
|
+
|
|
296
308
|
return comment
|
|
297
309
|
}
|
|
298
310
|
|
|
@@ -303,31 +315,16 @@ export default class ListLite extends Component {
|
|
|
303
315
|
this.putCommentFlatMode(commentData, srcData, 'append')
|
|
304
316
|
})
|
|
305
317
|
} else {
|
|
306
|
-
this.
|
|
318
|
+
this.importCommentsNest(srcData)
|
|
307
319
|
}
|
|
308
320
|
}
|
|
309
321
|
|
|
310
322
|
// 导入评论 · 嵌套模式
|
|
311
|
-
private
|
|
312
|
-
// 查找并导入所有子评论
|
|
313
|
-
const loadChildren = (parentC: Comment) => {
|
|
314
|
-
const children = srcData.filter(o => o.rid === parentC.data.id)
|
|
315
|
-
children.forEach((childData: CommentData) => {
|
|
316
|
-
const childC = this.createComment(childData)
|
|
317
|
-
childC.render()
|
|
318
|
-
|
|
319
|
-
// 插入到父评论中
|
|
320
|
-
parentC.putChild(childC)
|
|
321
|
-
|
|
322
|
-
// 递归加载子评论
|
|
323
|
-
loadChildren(childC)
|
|
324
|
-
})
|
|
325
|
-
}
|
|
326
|
-
|
|
323
|
+
private importCommentsNest(srcData: CommentData[]) {
|
|
327
324
|
// 遍历 root 评论
|
|
328
|
-
const
|
|
329
|
-
|
|
330
|
-
const rootC = this.createComment(
|
|
325
|
+
const rootNodes = ListNest.makeNestCommentNodeList(srcData, this.nestSortBy, this.conf.nestMax)
|
|
326
|
+
rootNodes.forEach((rootNode: ListNest.CommentNode) => {
|
|
327
|
+
const rootC = this.createComment(rootNode.comment, srcData)
|
|
331
328
|
rootC.render()
|
|
332
329
|
|
|
333
330
|
// 显示并播放渐入动画
|
|
@@ -335,10 +332,25 @@ export default class ListLite extends Component {
|
|
|
335
332
|
rootC.playFadeInAnim()
|
|
336
333
|
|
|
337
334
|
// 评论放入 comments 总表中
|
|
338
|
-
this.
|
|
335
|
+
this.commentList.push(rootC)
|
|
339
336
|
|
|
340
337
|
// 加载子评论
|
|
341
|
-
|
|
338
|
+
const that = this
|
|
339
|
+
;(function loadChildren(parentC: Comment, parentNode: ListNest.CommentNode) {
|
|
340
|
+
parentNode.children.forEach((node: ListNest.CommentNode) => {
|
|
341
|
+
const childD = node.comment
|
|
342
|
+
const childC = that.createComment(childD, srcData)
|
|
343
|
+
childC.render()
|
|
344
|
+
|
|
345
|
+
// 插入到父评论中
|
|
346
|
+
parentC.putChild(childC)
|
|
347
|
+
|
|
348
|
+
that.commentList.push(childC)
|
|
349
|
+
|
|
350
|
+
// 递归加载子评论
|
|
351
|
+
loadChildren(childC, node)
|
|
352
|
+
})
|
|
353
|
+
})(rootC, rootNode)
|
|
342
354
|
|
|
343
355
|
// 限高检测
|
|
344
356
|
rootC.checkHeightLimit()
|
|
@@ -346,18 +358,13 @@ export default class ListLite extends Component {
|
|
|
346
358
|
}
|
|
347
359
|
|
|
348
360
|
/** 导入评论 · 平铺模式 */
|
|
349
|
-
private putCommentFlatMode(cData: CommentData,
|
|
361
|
+
private putCommentFlatMode(cData: CommentData, ctxData: CommentData[], insertMode: 'append'|'prepend') {
|
|
350
362
|
if (cData.is_collapsed) cData.is_allow_reply = false
|
|
351
|
-
const comment = this.createComment(cData)
|
|
352
|
-
if (cData.rid !== 0) {
|
|
353
|
-
const rComment = srcData.find(o => o.id === cData.rid)
|
|
354
|
-
if (rComment) comment.replyTo = rComment
|
|
355
|
-
}
|
|
363
|
+
const comment = this.createComment(cData, ctxData)
|
|
356
364
|
comment.render()
|
|
357
365
|
|
|
358
366
|
// 将评论导入 comments 总表中
|
|
359
|
-
|
|
360
|
-
if (insertMode === 'prepend') this.comments.unshift(comment)
|
|
367
|
+
this.commentList.push(comment)
|
|
361
368
|
|
|
362
369
|
// 可见评论添加到界面
|
|
363
370
|
// 注:不可见评论用于显示 “引用内容”
|
|
@@ -378,17 +385,18 @@ export default class ListLite extends Component {
|
|
|
378
385
|
if (!this.flatMode) {
|
|
379
386
|
// 嵌套模式
|
|
380
387
|
const comment = this.createComment(commentData)
|
|
381
|
-
comment.render()
|
|
382
388
|
|
|
383
389
|
if (commentData.rid === 0) {
|
|
384
390
|
// root评论 新增
|
|
391
|
+
comment.render()
|
|
385
392
|
this.$commentsWrap.prepend(comment.getEl())
|
|
386
|
-
this.
|
|
393
|
+
this.commentList.push(comment)
|
|
387
394
|
} else {
|
|
388
395
|
// 子评论 新增
|
|
389
396
|
const parent = this.findComment(commentData.rid)
|
|
390
397
|
if (parent) {
|
|
391
|
-
|
|
398
|
+
comment.render()
|
|
399
|
+
parent.putChild(comment, (this.nestSortBy === 'DATE_ASC' ? 'append' : 'prepend'))
|
|
392
400
|
|
|
393
401
|
// 若父评论存在 “子评论部分” 限高,取消限高
|
|
394
402
|
comment.getParents().forEach((p) => {
|
|
@@ -403,7 +411,7 @@ export default class ListLite extends Component {
|
|
|
403
411
|
comment.playFadeInAnim() // 播放评论渐出动画
|
|
404
412
|
} else {
|
|
405
413
|
// 平铺模式
|
|
406
|
-
const comment = this.putCommentFlatMode(commentData, this.
|
|
414
|
+
const comment = this.putCommentFlatMode(commentData, this.commentDataList, 'prepend')
|
|
407
415
|
Ui.scrollIntoView(comment.getEl()) // 滚动到可见
|
|
408
416
|
}
|
|
409
417
|
|
|
@@ -415,47 +423,22 @@ export default class ListLite extends Component {
|
|
|
415
423
|
this.ctx.trigger('comments-loaded')
|
|
416
424
|
}
|
|
417
425
|
|
|
418
|
-
/** 遍历操作 Comment (包括子评论) */
|
|
419
|
-
protected eachComment(commentList: Comment[], action: (comment: Comment, levelList: Comment[]) => boolean|void) {
|
|
420
|
-
if (commentList.length === 0) return
|
|
421
|
-
commentList.every((item) => {
|
|
422
|
-
if (action(item, commentList) === false) return false
|
|
423
|
-
this.eachComment(item.getChildren(), action)
|
|
424
|
-
return true
|
|
425
|
-
})
|
|
426
|
-
}
|
|
427
|
-
|
|
428
426
|
/** 查找评论项 */
|
|
429
|
-
protected findComment(id: number
|
|
430
|
-
|
|
431
|
-
let comment: Comment|null = null
|
|
432
|
-
this.eachComment(src, (item) => {
|
|
433
|
-
if (item.data.id === id) {
|
|
434
|
-
comment = item
|
|
435
|
-
return false
|
|
436
|
-
}
|
|
437
|
-
return true
|
|
438
|
-
})
|
|
439
|
-
|
|
440
|
-
return comment
|
|
427
|
+
protected findComment(id: number): Comment|undefined {
|
|
428
|
+
return this.commentList.find(c => c.data.id === id)
|
|
441
429
|
}
|
|
442
430
|
|
|
443
431
|
/** 删除评论 */
|
|
444
|
-
public deleteComment(
|
|
445
|
-
let
|
|
446
|
-
if (typeof
|
|
447
|
-
findComment = this.findComment(
|
|
448
|
-
if (!findComment) throw Error(`未找到评论 ${
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
levelList.splice(levelList.indexOf(item), 1)
|
|
455
|
-
return false
|
|
456
|
-
}
|
|
457
|
-
return true
|
|
458
|
-
})
|
|
432
|
+
public deleteComment(_comment: number|Comment) {
|
|
433
|
+
let comment: Comment
|
|
434
|
+
if (typeof _comment === 'number') {
|
|
435
|
+
const findComment = this.findComment(_comment)
|
|
436
|
+
if (!findComment) throw Error(`未找到评论 ${_comment}`)
|
|
437
|
+
comment = findComment
|
|
438
|
+
} else comment = _comment
|
|
439
|
+
|
|
440
|
+
comment.getEl().remove()
|
|
441
|
+
this.commentList.splice(this.commentList.indexOf(comment), 1)
|
|
459
442
|
|
|
460
443
|
if (this.data) this.data.total -= 1 // 评论数减 1
|
|
461
444
|
|
|
@@ -466,7 +449,7 @@ export default class ListLite extends Component {
|
|
|
466
449
|
public clearAllComments() {
|
|
467
450
|
this.$commentsWrap.innerHTML = ''
|
|
468
451
|
this.data = undefined
|
|
469
|
-
this.
|
|
452
|
+
this.commentList = []
|
|
470
453
|
}
|
|
471
454
|
|
|
472
455
|
/** 更新未读数据 */
|
|
@@ -475,7 +458,7 @@ export default class ListLite extends Component {
|
|
|
475
458
|
|
|
476
459
|
// 高亮评论
|
|
477
460
|
if (this.unreadHighlight === true) {
|
|
478
|
-
this.
|
|
461
|
+
this.commentList.forEach((comment) => {
|
|
479
462
|
const notify = this.unread.find(o => o.comment_id === comment.data.id)
|
|
480
463
|
if (notify) {
|
|
481
464
|
comment.setUnread(true)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { CommentData } from '~/types/artalk-data'
|
|
2
|
+
|
|
3
|
+
export type CommentNode = {
|
|
4
|
+
id: number
|
|
5
|
+
comment: CommentData
|
|
6
|
+
children: CommentNode[]
|
|
7
|
+
parent?: CommentNode
|
|
8
|
+
level: number
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type SortByType = 'DATE_DESC'|'DATE_ASC'|'SRC_INDEX'|'VOTE_UP_DESC'
|
|
12
|
+
|
|
13
|
+
// 构建树状结构列表
|
|
14
|
+
export function makeNestCommentNodeList(srcData: CommentData[], sortBy: SortByType = 'DATE_DESC', nestMax = 2) {
|
|
15
|
+
const nodeList: CommentNode[] = []
|
|
16
|
+
|
|
17
|
+
const roots = srcData.filter((o) => o.rid === 0)
|
|
18
|
+
roots.forEach((root: CommentData) => {
|
|
19
|
+
const rootNode: CommentNode = {
|
|
20
|
+
id: root.id,
|
|
21
|
+
comment: root,
|
|
22
|
+
children: [],
|
|
23
|
+
level: 1,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
rootNode.parent = rootNode
|
|
27
|
+
nodeList.push(rootNode)
|
|
28
|
+
|
|
29
|
+
;(function loadChildren(parentNode: CommentNode) {
|
|
30
|
+
const children = srcData.filter((o) => o.rid === parentNode.id)
|
|
31
|
+
if (children.length === 0) return
|
|
32
|
+
if (parentNode.level >= nestMax) parentNode = parentNode.parent!
|
|
33
|
+
children.forEach((child: CommentData) => {
|
|
34
|
+
const childNode: CommentNode = {
|
|
35
|
+
id: child.id,
|
|
36
|
+
comment: child,
|
|
37
|
+
children: [],
|
|
38
|
+
parent: parentNode,
|
|
39
|
+
level: parentNode.level + 1,
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
parentNode.children.push(childNode)
|
|
43
|
+
loadChildren(childNode)
|
|
44
|
+
})
|
|
45
|
+
})(rootNode)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
// 排序
|
|
49
|
+
const sortFunc = (a: CommentNode, b: CommentNode): number => {
|
|
50
|
+
let v = a.id - b.id
|
|
51
|
+
if (sortBy === 'DATE_ASC') v = +new Date(a.comment.date) - +new Date(b.comment.date)
|
|
52
|
+
else if (sortBy === 'DATE_DESC') v = +new Date(b.comment.date) - +new Date(a.comment.date)
|
|
53
|
+
else if (sortBy === 'SRC_INDEX') v = srcData.indexOf(a.comment) - srcData.indexOf(b.comment)
|
|
54
|
+
else if (sortBy === 'VOTE_UP_DESC') v = b.comment.vote_up - a.comment.vote_up
|
|
55
|
+
return v
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
(function sortLevels(nodes: CommentNode[]) {
|
|
59
|
+
nodes.forEach((node) => {
|
|
60
|
+
node.children = node.children.sort(sortFunc)
|
|
61
|
+
sortLevels(node.children)
|
|
62
|
+
})
|
|
63
|
+
})(nodeList)
|
|
64
|
+
|
|
65
|
+
return nodeList
|
|
66
|
+
}
|
package/src/components/list.ts
CHANGED
|
@@ -112,11 +112,19 @@ export default class List extends ListLite {
|
|
|
112
112
|
super.onLoad(data, offset)
|
|
113
113
|
|
|
114
114
|
// 检测锚点跳转
|
|
115
|
-
this.checkGoToCommentByUrlHash()
|
|
115
|
+
if (!this.goToCommentFounded) this.checkGoToCommentByUrlHash()
|
|
116
|
+
|
|
117
|
+
// 防止评论框被吞
|
|
118
|
+
if (this.ctx.conf.editorTravel === true) {
|
|
119
|
+
this.ctx.trigger('editor-travel-back')
|
|
120
|
+
}
|
|
116
121
|
}
|
|
117
122
|
|
|
123
|
+
private goToCommentFounded = false
|
|
124
|
+
public goToCommentDelay = true
|
|
125
|
+
|
|
118
126
|
/** 跳到评论项位置 - 根据 `location.hash` */
|
|
119
|
-
public
|
|
127
|
+
public checkGoToCommentByUrlHash() {
|
|
120
128
|
let commentId = Number(Utils.getQueryParam('atk_comment')) // same as backend GetReplyLink()
|
|
121
129
|
if (!commentId) {
|
|
122
130
|
const match = window.location.hash.match(/#atk-comment-([0-9]+)/)
|
|
@@ -125,6 +133,14 @@ export default class List extends ListLite {
|
|
|
125
133
|
}
|
|
126
134
|
if (!commentId) return
|
|
127
135
|
|
|
136
|
+
const comment = this.findComment(commentId)
|
|
137
|
+
if (!comment) { // 若找不到评论
|
|
138
|
+
// 自动翻页
|
|
139
|
+
if (this.readMoreBtn) this.readMoreBtn.click()
|
|
140
|
+
else if (this.pagination) this.pagination.next()
|
|
141
|
+
return
|
|
142
|
+
}
|
|
143
|
+
|
|
128
144
|
// 已阅 API
|
|
129
145
|
const notifyKey = Utils.getQueryParam('atk_notify_key')
|
|
130
146
|
if (notifyKey) {
|
|
@@ -137,20 +153,24 @@ export default class List extends ListLite {
|
|
|
137
153
|
})
|
|
138
154
|
}
|
|
139
155
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
156
|
+
// 若父评论存在 “子评论部分” 限高,取消限高
|
|
157
|
+
comment.getParents().forEach((p) => {
|
|
158
|
+
if (p.$children) p.heightLimitRemove(p.$children)
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
const goTo = () => {
|
|
162
|
+
Ui.scrollIntoView(comment.getEl(), false)
|
|
163
|
+
|
|
164
|
+
comment.getEl().classList.remove('atk-flash-once')
|
|
165
|
+
window.setTimeout(() => {
|
|
166
|
+
comment.getEl().classList.add('atk-flash-once')
|
|
167
|
+
}, 150)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (!this.goToCommentDelay) goTo()
|
|
171
|
+
else window.setTimeout(() => goTo(), 350)
|
|
172
|
+
|
|
173
|
+
this.goToCommentFounded = true
|
|
154
174
|
}
|
|
155
175
|
|
|
156
176
|
/** 管理员设置页面信息 */
|
package/src/defaults.ts
CHANGED
package/src/style/comment.less
CHANGED
|
@@ -83,6 +83,27 @@
|
|
|
83
83
|
color: var(--at-color-main);
|
|
84
84
|
text-decoration: none;
|
|
85
85
|
}
|
|
86
|
+
|
|
87
|
+
&.atk-reply-at {
|
|
88
|
+
margin-left: 2px;
|
|
89
|
+
|
|
90
|
+
& > .atk-arrow:before {
|
|
91
|
+
content: '';
|
|
92
|
+
vertical-align: middle;
|
|
93
|
+
transform: rotate(90deg);
|
|
94
|
+
border-bottom: 4px solid var(--at-color-grey);
|
|
95
|
+
border-left: 3px solid transparent;
|
|
96
|
+
border-right: 3px solid transparent;
|
|
97
|
+
display: inline-block;
|
|
98
|
+
margin-top: -1px;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
& > .atk-nick {
|
|
102
|
+
color: var(--at-color-main);
|
|
103
|
+
cursor: pointer;
|
|
104
|
+
margin-left: 6px;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
86
107
|
}
|
|
87
108
|
|
|
88
109
|
.badge() {
|