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/dist/Artalk.css +1 -1
- package/dist/Artalk.es.js +281 -124
- package/dist/Artalk.iife.js +1 -1
- package/dist/Artalk.js +1 -1
- package/index.html +10 -8
- package/package.json +2 -2
- package/src/api/index.ts +16 -0
- package/src/artalk.ts +43 -10
- package/src/components/comment.ts +60 -25
- 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/components/pagination.ts +1 -1
- package/src/defaults.ts +3 -1
- package/src/lib/utils.ts +30 -0
- package/src/style/_color.less +2 -2
- package/src/style/comment.less +21 -0
- package/types/artalk-config.d.ts +5 -2
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:#
|
|
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: #
|
|
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
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
|
|
25
|
-
public conf
|
|
26
|
-
public $root
|
|
25
|
+
public ctx!: Context
|
|
26
|
+
public conf!: ArtalkConfig
|
|
27
|
+
public $root!: HTMLElement
|
|
27
28
|
|
|
28
|
-
public checkerLauncher
|
|
29
|
-
public editor
|
|
30
|
-
public list
|
|
31
|
-
public 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 =
|
|
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
|
|
27
|
-
private
|
|
28
|
-
|
|
28
|
+
public nestCurt: number
|
|
29
|
+
private nestMax: number // 最大嵌套层数
|
|
30
|
+
private children: Comment[] = []
|
|
29
31
|
|
|
30
|
-
public
|
|
31
|
-
public
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
131
|
+
this.$headerNick.append($nickA)
|
|
126
132
|
} else {
|
|
127
|
-
|
|
133
|
+
this.$headerNick.innerText = this.data.nick
|
|
128
134
|
}
|
|
129
135
|
|
|
130
|
-
|
|
136
|
+
this.$headerBadge = this.$el.querySelector<HTMLElement>('.atk-badge')!
|
|
131
137
|
if (this.data.badge_name) {
|
|
132
|
-
|
|
138
|
+
this.$headerBadge.innerText = this.data.badge_name
|
|
133
139
|
if (this.data.badge_color)
|
|
134
|
-
|
|
140
|
+
this.$headerBadge.style.backgroundColor = this.data.badge_color
|
|
135
141
|
} else {
|
|
136
|
-
|
|
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
|
-
|
|
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')
|
|
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.
|
|
361
|
+
childC.nestCurt = this.nestCurt + 1 // 嵌套层数 +1
|
|
330
362
|
this.children.push(childC)
|
|
331
363
|
|
|
332
|
-
this.getChildrenEl()
|
|
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
|
|
341
|
-
// console.log(this.
|
|
342
|
-
if (this.
|
|
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.
|
|
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() {
|
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)
|