artalk 2.2.7 → 2.2.10

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/index.html CHANGED
@@ -1,5 +1,5 @@
1
1
  <!DOCTYPE html>
2
- <html>
2
+ <html lang="zh-CN">
3
3
  <head>
4
4
  <meta charset="UTF-8">
5
5
  <title>Artalk DEMO</title>
@@ -16,7 +16,8 @@
16
16
  .title > a:hover { margin-left:0 }
17
17
  .title > a > span { opacity:0 }
18
18
  .title > a:hover>span { opacity:1 }
19
- .desc { font-size:12px;color:#757e91 }
19
+ .desc { font-size:12px;color:#697182 }
20
+ body.dark-mode .desc { color:#848da3 }
20
21
  .social { margin-top:20px }
21
22
  .social > a:not(:last-child) { margin-right:2px }
22
23
  @media only screen and (max-width: 768px) {
@@ -26,7 +27,7 @@
26
27
  }
27
28
  .float-btn { user-select: none;bottom: 25px;display: block;cursor: pointer;text-align: center;width: 60px;height: 37px;line-height: 37px;font-size: 13px;color: #757e91;background: #fff;opacity: 0.8;border-radius: 4px;border: 1px solid #eceff2;position: fixed; }
28
29
  .to-night { z-index: 99999;left: 25px;background: #1e2223;color: #fff; }
29
- body.dark-mode .to-night { background: #fff;border: 1px solid #2d3235;color: #0083ff; }
30
+ body.dark-mode .to-night { background: #006fd9;border: 1px solid #2d3235;color: #fff; }
30
31
  .to-top { right: 25px; }
31
32
  </style>
32
33
  <!--<link rel="stylesheet"
@@ -45,11 +46,11 @@
45
46
  <div class="title"><a href="https://github.com/ArtalkJS/Artalk" target="_blank"><span>『 </span>Artalk<span> 』</span></a></div>
46
47
  <div class="desc">一款简洁的自托管评论系统 <span id="ArtalkPV"></span></div>
47
48
  <div class="social">
48
- <a href="https://www.npmjs.com/package/artalk" target="_blank"><img src="https://img.shields.io/npm/v/artalk.svg?style=flat-square"></a>
49
- <a href="https://www.npmjs.com/package/artalk" target="_blank"><img src="https://img.shields.io/npm/dt/artalk.svg?style=flat-square"></a>
50
- <a href="https://github.com/ArtalkJS/Artalk/commits/master" target="_blank"><img src="https://img.shields.io/github/last-commit/ArtalkJS/Artalk?style=flat-square"></a>
51
- <a href="https://github.com/ArtalkJS/Artalk/blob/master/LICENSE" target="_blank"><img src="https://img.shields.io/github/license/ArtalkJS/Artalk.svg?style=flat-square"></a>
52
- <a href="https://github.com/ArtalkJS/Artalk" target="_blank"><img src="https://img.shields.io/github/stars/ArtalkJS/Artalk?style=flat-square"></a>
49
+ <a href="https://www.npmjs.com/package/artalk" aria-label="npm package version" target="_blank"><img src="https://img.shields.io/npm/v/artalk.svg?style=flat-square"></a>
50
+ <a href="https://www.npmjs.com/package/artalk" aria-label="npm package download number" target="_blank"><img src="https://img.shields.io/npm/dt/artalk.svg?style=flat-square"></a>
51
+ <a href="https://github.com/ArtalkJS/Artalk/commits/master" aria-label="last commit" target="_blank"><img src="https://img.shields.io/github/last-commit/ArtalkJS/Artalk?style=flat-square"></a>
52
+ <a href="https://github.com/ArtalkJS/Artalk/blob/master/LICENSE" aria-label="open source license" target="_blank"><img src="https://img.shields.io/github/license/ArtalkJS/Artalk.svg?style=flat-square"></a>
53
+ <a href="https://github.com/ArtalkJS/Artalk" aria-label="GitHub star number" target="_blank"><img src="https://img.shields.io/github/stars/ArtalkJS/Artalk?style=flat-square"></a>
53
54
  </div>
54
55
  </div>
55
56
  <div class="card atk-fade-in">
@@ -66,6 +67,7 @@
66
67
  pageTitle: 'Artalk Home',
67
68
  server: (import.meta.env.MODE === 'development') ? 'http://localhost:23366' : 'https://artalk.qwqaq.com',
68
69
  site: 'ArtalkDemo',
70
+ //useBackendConf: true,
69
71
  });
70
72
 
71
73
  window.artalk = artalk;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "artalk",
3
- "version": "2.2.7",
4
- "backendMinVersion": "2.1.2",
3
+ "version": "2.2.10",
4
+ "backendMinVersion": "2.1.4",
5
5
  "license": "LGPL-3.0",
6
6
  "description": "A Selfhosted Comment System",
7
7
  "keywords": [
package/src/api/index.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { CommentData, ListData, UserData, PageData, SiteData, NotifyData } from '~/types/artalk-data'
2
+ import ArtalkConfig from '~/types/artalk-config'
2
3
  import Context from '../context'
3
4
  import { Fetch, ToFormData, POST, GET } from './request'
4
5
  import * as Utils from '../lib/utils'
@@ -294,7 +295,22 @@ export default class Api {
294
295
 
295
296
  const json = await Fetch(this.ctx, `${this.baseURL}/img-upload`, init)
296
297
  return ((json.data || {}) as any) as { img_file: string, img_url: string }
298
+ }
299
+
300
+ /** 获取配置 */
301
+ public async conf() {
302
+ const data = await POST<any>(this.ctx, `${this.baseURL}/conf`)
303
+ const conf = (data.frontend_conf || {}) as ArtalkConfig
304
+
305
+ // Patch: `emoticons` config string to json
306
+ if (conf.emoticons && typeof conf.emoticons === "string") {
307
+ conf.emoticons = conf.emoticons.trim()
308
+ if (conf.emoticons.startsWith("[") || conf.emoticons.startsWith("{")) {
309
+ conf.emoticons = JSON.parse(conf.emoticons) // pase json
310
+ }
311
+ }
297
312
 
313
+ return conf
298
314
  }
299
315
 
300
316
  // ============================
package/src/artalk.ts CHANGED
@@ -13,6 +13,7 @@ import SidebarLayer from './components/sidebar-layer'
13
13
  import Layer, { GetLayerWrap } from './components/layer'
14
14
  import Api from './api'
15
15
  import * as Utils from './lib/utils'
16
+ import * as Ui from './lib/ui'
16
17
 
17
18
  /**
18
19
  * Artalk
@@ -21,18 +22,18 @@ import * as Utils from './lib/utils'
21
22
  export default class Artalk {
22
23
  public static readonly defaults: ArtalkConfig = defaults
23
24
 
24
- public ctx: Context
25
- public conf: ArtalkConfig
26
- public $root: HTMLElement
25
+ public ctx!: Context
26
+ public conf!: ArtalkConfig
27
+ public $root!: HTMLElement
27
28
 
28
- public checkerLauncher: CheckerLauncher
29
- public editor: Editor
30
- public list: List
31
- public sidebarLayer: SidebarLayer
29
+ public checkerLauncher!: CheckerLauncher
30
+ public editor!: Editor
31
+ public list!: List
32
+ public sidebarLayer!: SidebarLayer
32
33
 
33
34
  constructor (customConf: ArtalkConfig) {
34
35
  // 配置
35
- this.conf = { ...Artalk.defaults, ...customConf }
36
+ this.conf = Utils.mergeDeep(Artalk.defaults, customConf)
36
37
  this.conf.server = this.conf.server.replace(/\/$/, '').replace(/\/api\/?$/, '')
37
38
 
38
39
  // 默认 pageKey
@@ -46,6 +47,11 @@ export default class Artalk {
46
47
  this.conf.pageTitle = `${document.title}`
47
48
  }
48
49
 
50
+ // 列表显示模式
51
+ if (this.conf.nestMax && this.conf.nestMax <= 1) {
52
+ this.conf.flatMode = true
53
+ }
54
+
49
55
  // 装载元素
50
56
  if (!!this.conf.el && this.conf.el instanceof HTMLElement) {
51
57
  this.$root = this.conf.el
@@ -66,10 +72,25 @@ export default class Artalk {
66
72
  // 界面初始化
67
73
  this.$root.classList.add('artalk')
68
74
  this.$root.innerHTML = ''
69
- this.initLayer()
70
- this.initDarkMode()
71
75
 
76
+ // 初始化组件
77
+ if (this.conf.useBackendConf) {
78
+ // 复用后端的配置
79
+ // @ts-ignore
80
+ return (async () => {
81
+ await this.loadConfRemote()
82
+ this.initCore()
83
+ return this // Promise<Artalk>
84
+ })()
85
+ }
86
+
87
+ this.initCore()
88
+ }
89
+
90
+ private initCore() {
72
91
  // 组件初始化
92
+ this.initLayer()
93
+ this.initDarkMode()
73
94
  this.checkerLauncher = new CheckerLauncher(this.ctx)
74
95
  Utils.initMarked(this.ctx) // 初始化 marked
75
96
 
@@ -102,10 +123,22 @@ export default class Artalk {
102
123
  })
103
124
  }
104
125
 
126
+ /** 获取远程配置 */
127
+ private async loadConfRemote() {
128
+ Ui.showLoading(this.$root)
129
+ let backendConf = {}
130
+ try {
131
+ backendConf = await (new Api(this.ctx)).conf()
132
+ } catch (err) { console.error("配置远程获取失败", err) }
133
+ this.ctx.conf = Utils.mergeDeep(this.ctx.conf, backendConf)
134
+ Ui.hideLoading(this.$root)
135
+ }
136
+
105
137
  /** 事件绑定 · 初始化 */
106
138
  private initEventBind() {
107
139
  // 锚点快速跳转评论
108
140
  window.addEventListener('hashchange', () => {
141
+ this.list.goToCommentDelay = false
109
142
  this.list.checkGoToCommentByUrlHash()
110
143
  })
111
144
 
@@ -15,6 +15,8 @@ export default class Comment extends Component {
15
15
 
16
16
  public $main!: HTMLElement
17
17
  public $header!: HTMLElement
18
+ public $headerNick!: HTMLElement
19
+ public $headerBadge?: HTMLElement
18
20
  public $body!: HTMLElement
19
21
  public $content!: HTMLElement
20
22
  public $children!: HTMLElement|null
@@ -23,12 +25,15 @@ export default class Comment extends Component {
23
25
  public voteBtnDown?: ActionBtn
24
26
 
25
27
  public parent: Comment|null
26
- public nestedNum: number
27
- private maxNestingNum: number // 最多嵌套层数
28
- public children: Comment[] = []
28
+ public nestCurt: number
29
+ private nestMax: number // 最大嵌套层数
30
+ private children: Comment[] = []
29
31
 
30
- public replyTo?: CommentData // 回复对象(flatMode 用)
31
- public $replyTo?: HTMLElement
32
+ public flatMode: boolean = false
33
+ public replyTo?: CommentData // 回复对象
34
+
35
+ public $replyTo?: HTMLElement // 回复评论内容 (平铺下显示)
36
+ public $replyAt?: HTMLElement // 回复 AT(层级嵌套下显示)
32
37
 
33
38
  public afterRender?: () => void
34
39
 
@@ -43,13 +48,13 @@ export default class Comment extends Component {
43
48
  super(ctx)
44
49
 
45
50
  // 最大嵌套数
46
- this.maxNestingNum = ctx.conf.maxNesting || 3
51
+ this.nestMax = ctx.conf.nestMax || 3
47
52
 
48
53
  this.data = { ...data }
49
54
  this.data.date = this.data.date.replace(/-/g, '/') // 解决 Safari 日期解析 NaN 问题
50
55
 
51
56
  this.parent = null
52
- this.nestedNum = 1 // 现在已嵌套 n 层
57
+ this.nestCurt = 1 // 现在已嵌套 n 层
53
58
  }
54
59
 
55
60
  /** 渲染 UI */
@@ -70,6 +75,7 @@ export default class Comment extends Component {
70
75
  this.renderAvatar()
71
76
  this.renderHeader()
72
77
  this.renderContent()
78
+ this.renderReplyAt()
73
79
  this.renderReplyTo()
74
80
  this.renderPending()
75
81
  this.renderActionBtn()
@@ -117,28 +123,29 @@ export default class Comment extends Component {
117
123
  }
118
124
 
119
125
  private renderHeader() {
120
- const $nick = this.$el.querySelector<HTMLElement>('.atk-nick')!
126
+ this.$headerNick = this.$el.querySelector<HTMLElement>('.atk-nick')!
121
127
  if (this.data.link) {
122
128
  const $nickA = Utils.createElement<HTMLLinkElement>('<a target="_blank" rel="noreferrer noopener nofollow"></a>')
123
129
  $nickA.innerText = this.data.nick
124
130
  $nickA.href = this.data.link
125
- $nick.append($nickA)
131
+ this.$headerNick.append($nickA)
126
132
  } else {
127
- $nick.innerText = this.data.nick
133
+ this.$headerNick.innerText = this.data.nick
128
134
  }
129
135
 
130
- const $badge = this.$el.querySelector<HTMLElement>('.atk-badge')!
136
+ this.$headerBadge = this.$el.querySelector<HTMLElement>('.atk-badge')!
131
137
  if (this.data.badge_name) {
132
- $badge.innerText = this.data.badge_name
138
+ this.$headerBadge.innerText = this.data.badge_name
133
139
  if (this.data.badge_color)
134
- $badge.style.backgroundColor = this.data.badge_color
140
+ this.$headerBadge.style.backgroundColor = this.data.badge_color
135
141
  } else {
136
- $badge.remove()
142
+ this.$headerBadge.remove()
143
+ this.$headerBadge = undefined
137
144
  }
138
145
 
139
146
  if (this.data.is_pinned) {
140
147
  const $pinnedBadge = Utils.createElement(`<span class="atk-item atk-pinned-badge">置顶</span>`) // 置顶徽章
141
- $nick.insertAdjacentElement('afterend', $pinnedBadge)
148
+ this.$headerNick.insertAdjacentElement('afterend', $pinnedBadge)
142
149
  }
143
150
 
144
151
  const $date = this.$el.querySelector<HTMLElement>('.atk-date')!
@@ -189,8 +196,23 @@ export default class Comment extends Component {
189
196
  })
190
197
  }
191
198
 
199
+ // 层级嵌套模式显示 At
200
+ private renderReplyAt() {
201
+ if (this.flatMode || this.data.rid === 0) return // not 平铺模式 或 根评论
202
+ if (this.$replyAt) return // not 关闭显示 或 已经显示
203
+ if (!this.replyTo) return
204
+
205
+ this.$replyAt = Utils.createElement(`<span class="atk-item atk-reply-at"><span class="atk-arrow"></span><span class="atk-nick"></span></span>`)
206
+ this.$replyAt.querySelector<HTMLElement>('.atk-nick')!.innerText = `${this.replyTo.nick}`
207
+ this.$replyAt.onclick = () => { this.goToReplyComment() }
208
+
209
+ const $after = this.$headerBadge || this.$headerNick
210
+ $after.insertAdjacentElement('afterend', this.$replyAt)
211
+ }
212
+
192
213
  // 回复的对象
193
214
  private renderReplyTo() {
215
+ if (!this.flatMode) return // 仅平铺模式显示
194
216
  if (!this.replyTo) return
195
217
 
196
218
  this.$replyTo = Utils.createElement(`
@@ -198,13 +220,23 @@ export default class Comment extends Component {
198
220
  <div class="atk-meta">回复 <span class="atk-nick"></span>:</div>
199
221
  <div class="atk-content"></div>
200
222
  </div>`)
201
- this.$replyTo.querySelector<HTMLElement>('.atk-nick')!.innerText = `@${this.replyTo.nick}`
223
+ const $nick = this.$replyTo.querySelector<HTMLElement>('.atk-nick')!
224
+ $nick.innerText = `@${this.replyTo.nick}`
225
+ $nick.onclick = () => { this.goToReplyComment() }
202
226
  let replyContent = Utils.marked(this.ctx, this.replyTo.content)
203
227
  if (this.replyTo.is_collapsed) replyContent = '[已折叠]'
204
228
  this.$replyTo.querySelector<HTMLElement>('.atk-content')!.innerHTML = replyContent
205
229
  this.$body.prepend(this.$replyTo)
206
230
  }
207
231
 
232
+ public goToReplyComment() {
233
+ const origHash = window.location.hash
234
+ const modifyHash = `#atk-comment-${this.data.rid}`
235
+
236
+ window.location.hash = modifyHash
237
+ if (modifyHash === origHash) window.dispatchEvent(new Event('hashchange')) // 强制触发事件
238
+ }
239
+
208
240
  // 待审核状态
209
241
  private renderPending() {
210
242
  if (!this.data.is_pending) return
@@ -324,30 +356,33 @@ export default class Comment extends Component {
324
356
  return this.children
325
357
  }
326
358
 
327
- putChild(childC: Comment) {
359
+ putChild(childC: Comment, insertMode: 'append'|'prepend' = 'append') {
328
360
  childC.parent = this
329
- childC.nestedNum = this.nestedNum + 1 // 嵌套层数 +1
361
+ childC.nestCurt = this.nestCurt + 1 // 嵌套层数 +1
330
362
  this.children.push(childC)
331
363
 
332
- this.getChildrenEl().appendChild(childC.getEl())
364
+ const $children = this.getChildrenEl()
365
+ if (insertMode === 'append') $children.append(childC.getEl())
366
+ else if (insertMode === 'prepend') $children.prepend(childC.getEl())
367
+
333
368
  childC.playFadeInAnim()
334
369
 
335
370
  // 内容限高
336
371
  childC.checkHeightLimitArea('content')
337
372
  }
338
373
 
339
- getChildrenEl() {
340
- if (this.$children === null) {
341
- // console.log(this.nestedNum)
342
- if (this.nestedNum < this.maxNestingNum) {
374
+ getChildrenEl(): HTMLElement {
375
+ if (!this.$children) {
376
+ // console.log(this.nestCurt)
377
+ if (this.nestCurt < this.nestMax) {
343
378
  this.$children = Utils.createElement('<div class="atk-comment-children"></div>')
344
- this.$main.appendChild(this.$children)
379
+ this.$main.append(this.$children)
345
380
  } else if (this.parent) {
346
381
  this.$children = this.parent.getChildrenEl()
347
382
  }
348
383
  }
349
384
 
350
- return this.$children
385
+ return this.$children!
351
386
  }
352
387
 
353
388
  getParent() {
@@ -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.onHeaderInputChanged(field, inputEl))
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
- onHeaderInputChanged (field: string, inputEl: HTMLInputElement) {
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
- protected comments: Comment[] = []
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.comments.length <= 0
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(data: CommentData) {
287
- const comment = new Comment(this.ctx, data)
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.importCommentsNesting(srcData)
318
+ this.importCommentsNest(srcData)
307
319
  }
308
320
  }
309
321
 
310
322
  // 导入评论 · 嵌套模式
311
- private importCommentsNesting(srcData: CommentData[]) {
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 rootComments = srcData.filter((o) => o.rid === 0)
329
- rootComments.forEach((rootData: CommentData) => {
330
- const rootC = this.createComment(rootData)
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.comments.push(rootC)
335
+ this.commentList.push(rootC)
339
336
 
340
337
  // 加载子评论
341
- loadChildren(rootC)
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, srcData: CommentData[], insertMode: 'append'|'prepend') {
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
- if (insertMode === 'append') this.comments.push(comment)
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.comments.unshift(comment)
393
+ this.commentList.push(comment)
387
394
  } else {
388
395
  // 子评论 新增
389
396
  const parent = this.findComment(commentData.rid)
390
397
  if (parent) {
391
- parent.putChild(comment)
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.comments.map(c => c.data), 'prepend')
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, src?: Comment[]): Comment|null {
430
- if (!src) src = this.comments
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(comment: number|Comment) {
445
- let findComment: Comment|null
446
- if (typeof comment === 'number') {
447
- findComment = this.findComment(comment)
448
- if (!findComment) throw Error(`未找到评论 ${comment}`)
449
- } else findComment = comment
450
-
451
- findComment.getEl().remove()
452
- this.eachComment(this.comments, (item, levelList) => {
453
- if (item === findComment) {
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.comments = []
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.eachComment(this.comments, (comment) => {
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)