artalk 2.1.5 → 2.1.8
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 +209 -6
- package/dist/Artalk.iife.js +1 -1
- package/dist/Artalk.js +1 -1
- package/index.html +5 -5
- package/package.json +1 -1
- package/src/api/index.ts +35 -0
- package/src/artalk.ts +25 -2
- package/src/components/comment.ts +20 -3
- package/src/components/editor.ts +90 -1
- package/src/components/html/list.html +4 -2
- package/src/components/list-lite.ts +7 -0
- package/src/components/list.ts +60 -0
- package/src/components/sidebar-layer.ts +1 -1
- package/src/defaults.ts +2 -0
- package/src/style/comment.less +6 -0
- package/src/style/list.less +66 -0
- package/src/style/main.less +1 -1
- package/src/style/sidebar-layer.less +1 -0
- package/types/artalk-config.d.ts +6 -0
- package/types/artalk-data.d.ts +6 -0
- package/types/event.d.ts +1 -0
package/index.html
CHANGED
|
@@ -31,9 +31,9 @@
|
|
|
31
31
|
</style>
|
|
32
32
|
</head>
|
|
33
33
|
<body>
|
|
34
|
-
<div class="card billboard
|
|
34
|
+
<div class="card billboard atk-fade-in">
|
|
35
35
|
<div class="title"><a href="https://github.com/ArtalkJS/Artalk" target="_blank"><span>『 </span>Artalk<span> 』</span></a></div>
|
|
36
|
-
<div class="desc"
|
|
36
|
+
<div class="desc">一款简洁的自托管评论系统 <span id="ArtalkPV"></span></div>
|
|
37
37
|
<div class="social">
|
|
38
38
|
<a href="https://www.npmjs.com/package/artalk" target="_blank"><img src="https://img.shields.io/npm/v/artalk.svg?style=flat-square"></a>
|
|
39
39
|
<a href="https://www.npmjs.com/package/artalk" target="_blank"><img src="https://img.shields.io/npm/dt/artalk.svg?style=flat-square"></a>
|
|
@@ -42,11 +42,11 @@
|
|
|
42
42
|
<a href="https://github.com/ArtalkJS/Artalk" target="_blank"><img src="https://img.shields.io/github/stars/ArtalkJS/Artalk?style=flat-square"></a>
|
|
43
43
|
</div>
|
|
44
44
|
</div>
|
|
45
|
-
<div class="card
|
|
45
|
+
<div class="card atk-fade-in">
|
|
46
46
|
<div id="comments"></div>
|
|
47
47
|
</div>
|
|
48
|
-
<div class="float-btn to-night
|
|
49
|
-
<div class="float-btn to-top
|
|
48
|
+
<div class="float-btn to-night atk-fade-in">Blink</div>
|
|
49
|
+
<div class="float-btn to-top atk-fade-in" style="display: none;">TOP</div>
|
|
50
50
|
|
|
51
51
|
<script type="module">
|
|
52
52
|
import Artalk from './src/main'
|
package/package.json
CHANGED
package/src/api/index.ts
CHANGED
|
@@ -252,6 +252,41 @@ export default class Api {
|
|
|
252
252
|
return POST(this.ctx, `${this.baseURL}/mark-read`, params)
|
|
253
253
|
}
|
|
254
254
|
|
|
255
|
+
/** PV */
|
|
256
|
+
public async pv() {
|
|
257
|
+
const params: any = {
|
|
258
|
+
site_name: this.ctx.conf.site || '',
|
|
259
|
+
page_key: this.ctx.conf.pageKey || '',
|
|
260
|
+
page_title: this.ctx.conf.pageTitle || ''
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const p = await POST<any>(this.ctx, `${this.baseURL}/pv`, params)
|
|
264
|
+
return p.pv as number
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** 图片上传 */
|
|
268
|
+
public async imgUpload(file: File) {
|
|
269
|
+
const params: any = {
|
|
270
|
+
name: this.ctx.user.data.nick,
|
|
271
|
+
email: this.ctx.user.data.email,
|
|
272
|
+
page_key: this.ctx.conf.pageKey,
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (this.ctx.conf.site) params.site_name = this.ctx.conf.site
|
|
276
|
+
|
|
277
|
+
const form = ToFormData(params)
|
|
278
|
+
form.set('file', file)
|
|
279
|
+
|
|
280
|
+
const init: RequestInit = {
|
|
281
|
+
method: 'POST',
|
|
282
|
+
body: form
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const json = await Fetch(this.ctx, `${this.baseURL}/img-upload`, init)
|
|
286
|
+
return ((json.data || {}) as any) as { img_file: string, img_url: string }
|
|
287
|
+
|
|
288
|
+
}
|
|
289
|
+
|
|
255
290
|
// ============================
|
|
256
291
|
// 验证码
|
|
257
292
|
// ============================
|
package/src/artalk.ts
CHANGED
|
@@ -11,6 +11,7 @@ import SidebarLayer from './components/sidebar-layer'
|
|
|
11
11
|
|
|
12
12
|
import { GetLayerWrap } from './components/layer'
|
|
13
13
|
import { EventPayloadMap, Handler } from '~/types/event'
|
|
14
|
+
import Api from './api'
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* Artalk
|
|
@@ -76,6 +77,9 @@ export default class Artalk {
|
|
|
76
77
|
|
|
77
78
|
// 事件绑定初始化
|
|
78
79
|
this.initEventBind()
|
|
80
|
+
|
|
81
|
+
// 其他
|
|
82
|
+
this.initPV()
|
|
79
83
|
}
|
|
80
84
|
|
|
81
85
|
/** 事件绑定 · 初始化 */
|
|
@@ -93,8 +97,12 @@ export default class Artalk {
|
|
|
93
97
|
|
|
94
98
|
// for layer
|
|
95
99
|
const { $wrap: $layerWrap } = GetLayerWrap(this.ctx)
|
|
96
|
-
if ($layerWrap)
|
|
97
|
-
|
|
100
|
+
if ($layerWrap) $layerWrap.querySelectorAll<HTMLElement>(`[atk-only-admin-show]`).forEach(item => items.push(item))
|
|
101
|
+
|
|
102
|
+
// for sidebar
|
|
103
|
+
// TODO: 这个其实应该写在 packages/artalk-sidebar 里面的
|
|
104
|
+
const $sidebarEl = document.querySelector<HTMLElement>('.atk-sidebar')
|
|
105
|
+
if ($sidebarEl) $sidebarEl.querySelectorAll<HTMLElement>(`[atk-only-admin-show]`).forEach(item => items.push(item))
|
|
98
106
|
|
|
99
107
|
items.forEach(($item: HTMLElement) => {
|
|
100
108
|
if (this.ctx.user.data.isAdmin)
|
|
@@ -140,9 +148,24 @@ export default class Artalk {
|
|
|
140
148
|
/** 暗黑模式 · 设定 */
|
|
141
149
|
public setDarkMode(darkMode: boolean) {
|
|
142
150
|
this.ctx.conf.darkMode = darkMode
|
|
151
|
+
this.ctx.trigger('conf-updated')
|
|
143
152
|
this.initDarkMode()
|
|
144
153
|
}
|
|
145
154
|
|
|
155
|
+
/** PV */
|
|
156
|
+
public async initPV() {
|
|
157
|
+
if (!this.conf.pvEl || !document.querySelector(this.conf.pvEl))
|
|
158
|
+
return
|
|
159
|
+
|
|
160
|
+
const $pv = document.querySelector<HTMLElement>(this.conf.pvEl)!
|
|
161
|
+
// $pv.innerText = '-' // 默认占位符
|
|
162
|
+
const pvNum = await new Api(this.ctx).pv()
|
|
163
|
+
if (Number.isNaN(Number(pvNum)))
|
|
164
|
+
return
|
|
165
|
+
|
|
166
|
+
$pv.innerText = String(pvNum)
|
|
167
|
+
}
|
|
168
|
+
|
|
146
169
|
/** 监听事件 */
|
|
147
170
|
public on<K extends keyof EventPayloadMap>(name: K, handler: Handler<EventPayloadMap[K]>) {
|
|
148
171
|
this.ctx.on(name, handler, 'external')
|
|
@@ -136,6 +136,11 @@ export default class Comment extends Component {
|
|
|
136
136
|
$badge.remove()
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
+
if (this.data.is_pinned) {
|
|
140
|
+
const $pinnedBadge = Utils.createElement(`<span class="atk-item atk-pinned-badge">置顶</span>`) // 置顶徽章
|
|
141
|
+
$nick.insertAdjacentElement('afterend', $pinnedBadge)
|
|
142
|
+
}
|
|
143
|
+
|
|
139
144
|
const $date = this.$el.querySelector<HTMLElement>('.atk-date')!
|
|
140
145
|
$date.innerText = this.getDateFormatted()
|
|
141
146
|
$date.setAttribute('data-atk-comment-date', String(+new Date(this.data.date)))
|
|
@@ -272,6 +277,16 @@ export default class Comment extends Component {
|
|
|
272
277
|
delBtn.setClick(() => {
|
|
273
278
|
this.adminDelete(delBtn)
|
|
274
279
|
})
|
|
280
|
+
|
|
281
|
+
// 绑定置顶按钮事件
|
|
282
|
+
const pinnedBtn = new ActionBtn({
|
|
283
|
+
text: () => (this.data.is_pinned ? '取消置顶' : '置顶'),
|
|
284
|
+
adminOnly: true
|
|
285
|
+
})
|
|
286
|
+
pinnedBtn.appendTo(this.$actions)
|
|
287
|
+
pinnedBtn.setClick(() => {
|
|
288
|
+
this.adminEdit('pinned', pendingBtn)
|
|
289
|
+
})
|
|
275
290
|
}
|
|
276
291
|
//#endregion
|
|
277
292
|
|
|
@@ -404,9 +419,9 @@ export default class Comment extends Component {
|
|
|
404
419
|
})
|
|
405
420
|
}
|
|
406
421
|
|
|
407
|
-
/** 管理员 -
|
|
408
|
-
adminEdit(type: 'collapsed'|'pending', btnElem: ActionBtn) {
|
|
409
|
-
if (btnElem.isLoading) return //
|
|
422
|
+
/** 管理员 - 评论状态修改 */
|
|
423
|
+
adminEdit(type: 'collapsed'|'pending'|'pinned', btnElem: ActionBtn) {
|
|
424
|
+
if (btnElem.isLoading) return // 若正在修改中
|
|
410
425
|
|
|
411
426
|
btnElem.setLoading(true, '修改中...')
|
|
412
427
|
|
|
@@ -416,6 +431,8 @@ export default class Comment extends Component {
|
|
|
416
431
|
modify.is_collapsed = !modify.is_collapsed
|
|
417
432
|
} else if (type === 'pending') {
|
|
418
433
|
modify.is_pending = !modify.is_pending
|
|
434
|
+
} else if (type === 'pinned') {
|
|
435
|
+
modify.is_pinned = !modify.is_pinned
|
|
419
436
|
}
|
|
420
437
|
|
|
421
438
|
new Api(this.ctx).commentEdit(modify).then((comment) => {
|
package/src/components/editor.ts
CHANGED
|
@@ -21,6 +21,7 @@ export default class Editor extends Component {
|
|
|
21
21
|
public $plugWrap: HTMLElement
|
|
22
22
|
public $bottom: HTMLElement
|
|
23
23
|
public $plugBtnWrap: HTMLElement
|
|
24
|
+
public $imgUploadBtn?: HTMLElement
|
|
24
25
|
public $submitBtn: HTMLButtonElement
|
|
25
26
|
public $notifyWrap: HTMLElement
|
|
26
27
|
|
|
@@ -63,6 +64,7 @@ export default class Editor extends Component {
|
|
|
63
64
|
this.ctx.on('editor-notify', (f) => (this.showNotify(f.msg, f.type)))
|
|
64
65
|
this.ctx.on('editor-travel', ($el) => (this.travel($el)))
|
|
65
66
|
this.ctx.on('editor-travel-back', () => (this.travelBack()))
|
|
67
|
+
this.ctx.on('conf-updated', () => (this.refreshUploadBtn()))
|
|
66
68
|
}
|
|
67
69
|
|
|
68
70
|
initLocalStorage () {
|
|
@@ -200,7 +202,7 @@ export default class Editor extends Component {
|
|
|
200
202
|
// 依次实例化 plug
|
|
201
203
|
this.LOADABLE_PLUG_LIST.forEach((PlugObj) => {
|
|
202
204
|
// 切换按钮
|
|
203
|
-
const btnElem = Utils.createElement(`<span class="atk-plug-btn">${PlugObj.BtnHTML}</span>`)
|
|
205
|
+
const btnElem = Utils.createElement(`<span class="atk-plug-btn" data-plug-name="${PlugObj.Name}">${PlugObj.BtnHTML}</span>`)
|
|
204
206
|
this.$plugBtnWrap.appendChild(btnElem)
|
|
205
207
|
btnElem.addEventListener('click', () => {
|
|
206
208
|
let plug = this.plugList[PlugObj.Name]
|
|
@@ -244,6 +246,8 @@ export default class Editor extends Component {
|
|
|
244
246
|
btnElem.classList.add('active')
|
|
245
247
|
})
|
|
246
248
|
})
|
|
249
|
+
|
|
250
|
+
this.initImgUploadBtn()
|
|
247
251
|
}
|
|
248
252
|
|
|
249
253
|
/** 关闭编辑器插件 */
|
|
@@ -253,6 +257,91 @@ export default class Editor extends Component {
|
|
|
253
257
|
this.openedPlugName = null
|
|
254
258
|
}
|
|
255
259
|
|
|
260
|
+
/** 允许的图片格式 */
|
|
261
|
+
allowImgExts = ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'svg', 'webp']
|
|
262
|
+
|
|
263
|
+
/** 初始化图片上传按钮 */
|
|
264
|
+
initImgUploadBtn() {
|
|
265
|
+
this.$imgUploadBtn = Utils.createElement(`<span class="atk-plug-btn">图片</span>`)
|
|
266
|
+
this.$plugBtnWrap.querySelector('[data-plug-name="preview"]')!.before(this.$imgUploadBtn) // 显示在预览图标之前
|
|
267
|
+
|
|
268
|
+
this.$imgUploadBtn.onclick = () => {
|
|
269
|
+
// 选择图片
|
|
270
|
+
const $input = document.createElement('input')
|
|
271
|
+
$input.type = 'file'
|
|
272
|
+
$input.accept = this.allowImgExts.map(o => `.${o}`).join(',')
|
|
273
|
+
$input.onchange = () => {
|
|
274
|
+
(async () => { // 解决阻塞 UI 问题
|
|
275
|
+
if (!$input.files || $input.files.length === 0) return
|
|
276
|
+
const file = $input.files[0]
|
|
277
|
+
this.uploadImg(file)
|
|
278
|
+
})()
|
|
279
|
+
}
|
|
280
|
+
$input.click() // 显示选择图片对话框
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// @link https://developer.mozilla.org/zh-CN/docs/Web/API/HTML_Drag_and_Drop_API/File_drag_and_drop
|
|
284
|
+
// 阻止浏览器的默认释放行为
|
|
285
|
+
this.$textarea.addEventListener('dragover', (evt) => {
|
|
286
|
+
evt.stopPropagation()
|
|
287
|
+
evt.preventDefault()
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
this.$textarea.addEventListener('drop', (evt) => {
|
|
291
|
+
if (evt.dataTransfer?.files) {
|
|
292
|
+
evt.preventDefault()
|
|
293
|
+
for (let i = 0; i < evt.dataTransfer.files.length; i++) {
|
|
294
|
+
const file = evt.dataTransfer.files[i]
|
|
295
|
+
this.uploadImg(file)
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
})
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** 刷新图片上传按钮 */
|
|
302
|
+
refreshUploadBtn() {
|
|
303
|
+
if (!this.$imgUploadBtn) return
|
|
304
|
+
|
|
305
|
+
if (!this.ctx.conf.imgUpload) {
|
|
306
|
+
this.$imgUploadBtn.setAttribute('atk-only-admin-show', '')
|
|
307
|
+
this.ctx.trigger('check-admin-show-el')
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
async uploadImg(file: File) {
|
|
312
|
+
const fileExt = /[^.]+$/.exec(file.name)
|
|
313
|
+
if (!fileExt || !this.allowImgExts.includes(fileExt[0])) return
|
|
314
|
+
|
|
315
|
+
// 插入占位加载文字
|
|
316
|
+
const uploadPlaceholderTxt = `\n`
|
|
317
|
+
this.insertContent(uploadPlaceholderTxt)
|
|
318
|
+
|
|
319
|
+
// 上传图片
|
|
320
|
+
let resp: any
|
|
321
|
+
try {
|
|
322
|
+
resp = await new Api(this.ctx).imgUpload(file)
|
|
323
|
+
} catch (err: any) {
|
|
324
|
+
console.error(err)
|
|
325
|
+
this.showNotify(`图片上传失败,${err.msg}`, 'e')
|
|
326
|
+
}
|
|
327
|
+
if (!!resp && resp.img_url) {
|
|
328
|
+
let imgURL = resp.img_url as string
|
|
329
|
+
if (imgURL.startsWith('.') || (imgURL.startsWith('/') && !imgURL.startsWith('//'))) {
|
|
330
|
+
// 若为相对路径,加上 artalk server
|
|
331
|
+
imgURL = `${this.ctx.conf.server.replace(/\/api\/?$/, '')}/${imgURL.replace(/^\//, '')}`
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// 上传成功插入图片
|
|
335
|
+
this.$textarea.value = this.$textarea.value.replace(uploadPlaceholderTxt, `\n`)
|
|
336
|
+
} else {
|
|
337
|
+
// 上传失败删除加载文字
|
|
338
|
+
this.$textarea.value = this.$textarea.value.replace(uploadPlaceholderTxt, '')
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// 更新内容保存到 localStorage
|
|
342
|
+
this.saveContent()
|
|
343
|
+
}
|
|
344
|
+
|
|
256
345
|
insertContent (val: string) {
|
|
257
346
|
if ((document as any).selection) {
|
|
258
347
|
this.$textarea.focus();
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
<div class="atk-list">
|
|
2
2
|
<div class="atk-list-header">
|
|
3
3
|
<div class="atk-comment-count">
|
|
4
|
-
<
|
|
5
|
-
|
|
4
|
+
<div class="atk-text">
|
|
5
|
+
<span class="atk-comment-count-num">0</span>
|
|
6
|
+
条评论
|
|
7
|
+
</div>
|
|
6
8
|
</div>
|
|
7
9
|
<div class="atk-right-action">
|
|
8
10
|
<span data-action="admin-close-comment" class="atk-hide" atk-only-admin-show>关闭评论</span>
|
|
@@ -119,6 +119,11 @@ export default class ListLite extends Component {
|
|
|
119
119
|
// 版本检测
|
|
120
120
|
if (this.ctx.conf.versionCheck && this.versionCheck(data.api_version)) return
|
|
121
121
|
|
|
122
|
+
// 图片上传功能
|
|
123
|
+
if (data.conf && typeof data.conf.img_upload === "boolean") {
|
|
124
|
+
this.ctx.conf.imgUpload = data.conf.img_upload
|
|
125
|
+
}
|
|
126
|
+
|
|
122
127
|
// 导入数据
|
|
123
128
|
this.importComments(data.comments)
|
|
124
129
|
|
|
@@ -130,6 +135,7 @@ export default class ListLite extends Component {
|
|
|
130
135
|
|
|
131
136
|
this.ctx.trigger('unread-update', { notifies: data.unread || [] })
|
|
132
137
|
this.ctx.trigger('comments-loaded')
|
|
138
|
+
this.ctx.trigger('conf-updated')
|
|
133
139
|
|
|
134
140
|
if (this.onAfterLoad) this.onAfterLoad(data)
|
|
135
141
|
}
|
|
@@ -481,6 +487,7 @@ export default class ListLite extends Component {
|
|
|
481
487
|
ignoreBtn.onclick = () => {
|
|
482
488
|
Ui.setError(this.ctx, null)
|
|
483
489
|
this.ctx.conf.versionCheck = false
|
|
490
|
+
this.ctx.trigger('conf-updated')
|
|
484
491
|
this.fetchComments(0)
|
|
485
492
|
}
|
|
486
493
|
errEl.append(ignoreBtn)
|
package/src/components/list.ts
CHANGED
|
@@ -12,6 +12,8 @@ export default class List extends ListLite {
|
|
|
12
12
|
private $closeCommentBtn!: HTMLElement
|
|
13
13
|
private $openSidebarBtn!: HTMLElement
|
|
14
14
|
private $unreadBadge!: HTMLElement
|
|
15
|
+
private $commentCount!: HTMLElement
|
|
16
|
+
private $dropdownWrap?: HTMLElement
|
|
15
17
|
|
|
16
18
|
constructor (ctx: Context) {
|
|
17
19
|
const el = Utils.createElement(ListHTML)
|
|
@@ -40,6 +42,10 @@ export default class List extends ListLite {
|
|
|
40
42
|
// 操作按钮
|
|
41
43
|
this.initListActionBtn()
|
|
42
44
|
|
|
45
|
+
// 下拉选择层
|
|
46
|
+
this.$commentCount = this.$el.querySelector('.atk-comment-count')!
|
|
47
|
+
this.initDropdown()
|
|
48
|
+
|
|
43
49
|
// copyright
|
|
44
50
|
this.$el.querySelector<HTMLElement>('.atk-copyright')!.innerHTML = `Powered By <a href="https://artalk.js.org" target="_blank" title="Artalk v${ARTALK_VERSION}">Artalk</a>`
|
|
45
51
|
|
|
@@ -171,4 +177,58 @@ export default class List extends ListLite {
|
|
|
171
177
|
this.$unreadBadge.style.display = 'none'
|
|
172
178
|
}
|
|
173
179
|
}
|
|
180
|
+
|
|
181
|
+
/** 初始化选择下拉层 */
|
|
182
|
+
protected initDropdown() {
|
|
183
|
+
this.$dropdownWrap = this.$commentCount
|
|
184
|
+
this.$commentCount.classList.add('atk-dropdown-wrap')
|
|
185
|
+
|
|
186
|
+
// 插入图标
|
|
187
|
+
this.$dropdownWrap.append(Utils.createElement(`<span class="atk-arrow-down-icon"></span>`))
|
|
188
|
+
|
|
189
|
+
const reloadUseParamsEditor = (func: (p: any) => void) => {
|
|
190
|
+
this.paramsEditor = (p) => { func(p) }
|
|
191
|
+
this.fetchComments(0)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// 下拉列表
|
|
195
|
+
const dropdownList = [
|
|
196
|
+
['最新', () => { reloadUseParamsEditor(p => { p.sort_by = 'date_desc' }) }],
|
|
197
|
+
['最热', () => { reloadUseParamsEditor(p => { p.sort_by = 'vote' }) }],
|
|
198
|
+
['最早', () => { reloadUseParamsEditor(p => { p.sort_by = 'date_asc' }) }],
|
|
199
|
+
['作者', () => { reloadUseParamsEditor(p => { p.view_only_admin = true }) }],
|
|
200
|
+
]
|
|
201
|
+
|
|
202
|
+
// 列表项点击事件
|
|
203
|
+
let curtActive = 0 // 当前选中
|
|
204
|
+
const onItemClick = (i: number, $item: HTMLElement, name: string, action: Function) => {
|
|
205
|
+
action()
|
|
206
|
+
|
|
207
|
+
// set active
|
|
208
|
+
curtActive = i
|
|
209
|
+
$dropdown.querySelectorAll('.active').forEach((e) => { e.classList.remove('active') })
|
|
210
|
+
$item.classList.add('active')
|
|
211
|
+
|
|
212
|
+
// 关闭层 (临时消失,取消 :hover)
|
|
213
|
+
$dropdown.style.display = 'none'
|
|
214
|
+
setTimeout(() => { $dropdown.style.display = '' }, 80)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// 生成列表元素
|
|
218
|
+
const $dropdown = Utils.createElement(`<ul class="atk-dropdown atk-fade-in"></ul>`)
|
|
219
|
+
dropdownList.forEach((item, i) => {
|
|
220
|
+
const name = item[0] as string
|
|
221
|
+
const action = item[1] as Function
|
|
222
|
+
|
|
223
|
+
const $item = Utils.createElement(`<li class="atk-dropdown-item"><span></span></li>`)
|
|
224
|
+
const $link = $item.querySelector<HTMLElement>('span')!
|
|
225
|
+
$link.innerText = name
|
|
226
|
+
$link.onclick = () => { onItemClick(i, $item, name, action) }
|
|
227
|
+
$dropdown.append($item)
|
|
228
|
+
|
|
229
|
+
if (i === curtActive) $item.classList.add('active') // 默认选中项
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
this.$dropdownWrap.append($dropdown)
|
|
233
|
+
}
|
|
174
234
|
}
|
package/src/defaults.ts
CHANGED
|
@@ -17,6 +17,7 @@ const defaults: ArtalkConfig = {
|
|
|
17
17
|
vote: true,
|
|
18
18
|
voteDown: false,
|
|
19
19
|
uaBadge: true,
|
|
20
|
+
pvEl: '#ArtalkPV',
|
|
20
21
|
flatMode: 'auto',
|
|
21
22
|
maxNesting: 3,
|
|
22
23
|
gravatar: {
|
|
@@ -35,6 +36,7 @@ const defaults: ArtalkConfig = {
|
|
|
35
36
|
children: 400,
|
|
36
37
|
},
|
|
37
38
|
|
|
39
|
+
imgUpload: true,
|
|
38
40
|
reqTimeout: 15000,
|
|
39
41
|
versionCheck: true,
|
|
40
42
|
}
|
package/src/style/comment.less
CHANGED
package/src/style/list.less
CHANGED
|
@@ -7,6 +7,72 @@
|
|
|
7
7
|
padding: 10px 10px 5px 10px;
|
|
8
8
|
border-bottom: 1px solid var(--at-color-border);
|
|
9
9
|
|
|
10
|
+
.atk-text {
|
|
11
|
+
display: inline-block;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/* 下拉选择层 */
|
|
15
|
+
.atk-dropdown-wrap {
|
|
16
|
+
position: relative;
|
|
17
|
+
cursor: pointer;
|
|
18
|
+
|
|
19
|
+
.atk-arrow-down-icon {
|
|
20
|
+
cursor: pointer;
|
|
21
|
+
vertical-align: middle;
|
|
22
|
+
border-top: 5px solid var(--at-color-grey);
|
|
23
|
+
border-left: 3px solid transparent;
|
|
24
|
+
border-right: 3px solid transparent;
|
|
25
|
+
margin-top: -1px;
|
|
26
|
+
margin-left: 0.4rem;
|
|
27
|
+
display: inline-block;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
&:hover {
|
|
31
|
+
.atk-dropdown {
|
|
32
|
+
display: block;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.atk-dropdown {
|
|
37
|
+
z-index: 888;
|
|
38
|
+
display: none;
|
|
39
|
+
height: auto !important;
|
|
40
|
+
max-height: calc(100vh - 2.7rem);
|
|
41
|
+
overflow-y: auto;
|
|
42
|
+
position: absolute;
|
|
43
|
+
top: 100%;
|
|
44
|
+
right: 0;
|
|
45
|
+
width: 100%;
|
|
46
|
+
background-color: var(--at-color-bg);
|
|
47
|
+
padding: 0.6rem 0;
|
|
48
|
+
border: 1px solid var(--at-color-border);
|
|
49
|
+
text-align: center;
|
|
50
|
+
border-radius: 0.25rem;
|
|
51
|
+
white-space: nowrap;
|
|
52
|
+
margin: 0;
|
|
53
|
+
list-style: none;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.atk-dropdown-item {
|
|
57
|
+
span, a {
|
|
58
|
+
display: block;
|
|
59
|
+
line-height: 2rem;
|
|
60
|
+
position: relative;
|
|
61
|
+
border-bottom: none;
|
|
62
|
+
font-weight: 400;
|
|
63
|
+
padding: 0 1.5rem;
|
|
64
|
+
|
|
65
|
+
&:hover {
|
|
66
|
+
color: var(--at-color-main);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
&.active span, a {
|
|
71
|
+
color: var(--at-color-main);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
10
76
|
.atk-comment-count {
|
|
11
77
|
font-size: 15px;
|
|
12
78
|
|
package/src/style/main.less
CHANGED
package/types/artalk-config.d.ts
CHANGED
|
@@ -62,6 +62,9 @@ export default interface ArtalkConfig {
|
|
|
62
62
|
/** 评论投票反对按钮 */
|
|
63
63
|
voteDown?: boolean
|
|
64
64
|
|
|
65
|
+
/** PV 元素 Selector */
|
|
66
|
+
pvEl?: string
|
|
67
|
+
|
|
65
68
|
/** 暗黑模式 */
|
|
66
69
|
darkMode?: boolean
|
|
67
70
|
|
|
@@ -77,6 +80,9 @@ export default interface ArtalkConfig {
|
|
|
77
80
|
/** 显示 UA 徽标 */
|
|
78
81
|
uaBadge?: boolean
|
|
79
82
|
|
|
83
|
+
/** 图片上传功能 */
|
|
84
|
+
imgUpload?: boolean
|
|
85
|
+
|
|
80
86
|
/** 版本检测 */
|
|
81
87
|
versionCheck?: boolean
|
|
82
88
|
}
|
package/types/artalk-data.d.ts
CHANGED
|
@@ -29,6 +29,9 @@ export interface CommentData {
|
|
|
29
29
|
/** 是否待审 */
|
|
30
30
|
is_pending?: boolean
|
|
31
31
|
|
|
32
|
+
/** 是否置顶 */
|
|
33
|
+
is_pinned?: boolean
|
|
34
|
+
|
|
32
35
|
/** 是否为管理员 */
|
|
33
36
|
is_admin?: boolean
|
|
34
37
|
|
|
@@ -84,6 +87,9 @@ export interface ListData {
|
|
|
84
87
|
|
|
85
88
|
/** API 版本 */
|
|
86
89
|
api_version: ApiVersionData
|
|
90
|
+
|
|
91
|
+
/** 后端配置 */
|
|
92
|
+
conf: any // TODO
|
|
87
93
|
}
|
|
88
94
|
|
|
89
95
|
export interface PageData {
|
package/types/event.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ export interface EventPayloadMap {
|
|
|
8
8
|
'editor-submit': undefined // 编辑器提交时
|
|
9
9
|
'editor-submitted': undefined // 编辑器提交后
|
|
10
10
|
'user-changed': LocalUser // 本地用户数据变更时
|
|
11
|
+
'conf-updated': undefined // Artalk 配置变更时
|
|
11
12
|
|
|
12
13
|
// List 操作(外部:不建议 listen,仅 trigger)
|
|
13
14
|
'list-reload': undefined // 重新加载 List
|