artalk 2.1.7 → 2.2.0
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/LICENSE +165 -0
- package/dist/Artalk.css +1 -1
- package/dist/Artalk.es.js +337 -83
- package/dist/Artalk.iife.js +1 -1
- package/dist/Artalk.js +1 -1
- package/package.json +14 -13
- package/src/api/index.ts +35 -0
- package/src/api/request.ts +1 -0
- package/src/artalk.ts +18 -0
- package/src/components/editor.ts +120 -5
- package/src/components/layer.ts +19 -8
- package/src/components/list-lite.ts +19 -10
- package/src/components/list.ts +6 -3
- package/src/components/sidebar-layer.ts +24 -2
- package/src/context.ts +3 -0
- package/src/defaults.ts +2 -0
- package/src/lib/checker/admin-checker.ts +4 -4
- package/src/lib/checker/captcha-checker.ts +52 -6
- package/src/lib/checker/index.ts +65 -23
- package/src/lib/ui.ts +2 -1
- package/src/lib/utils.ts +56 -35
- package/src/style/comment.less +3 -0
- package/src/style/list.less +7 -4
- package/src/style/main.less +35 -0
- package/tsconfig.json +1 -16
- package/types/artalk-config.d.ts +6 -0
- package/types/artalk-data.d.ts +3 -0
- package/types/event.d.ts +2 -0
- package/types/vite-env.d.ts +0 -1
package/package.json
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "artalk",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"
|
|
3
|
+
"version": "2.2.0",
|
|
4
|
+
"backendMinVersion": "2.1.1",
|
|
5
|
+
"license": "LGPL-3.0",
|
|
5
6
|
"description": "A Selfhosted Comment System",
|
|
6
7
|
"keywords": [
|
|
7
8
|
"comment-system",
|
|
@@ -22,16 +23,6 @@
|
|
|
22
23
|
"jsdelivr": "./dist/Artalk.iife.js",
|
|
23
24
|
"module": "./dist/Artalk.es.js",
|
|
24
25
|
"types": "./types/index.d.ts",
|
|
25
|
-
"scripts": {
|
|
26
|
-
"dev": "vite",
|
|
27
|
-
"build": "yarn lint && vite build",
|
|
28
|
-
"build:demo": "vite build --config vite-demo.config.ts",
|
|
29
|
-
"serve": "vite preview",
|
|
30
|
-
"deploy": "gh-pages -d deploy",
|
|
31
|
-
"predeploy": "yarn run build && yarn run build:demo && yarn run copy:deploy",
|
|
32
|
-
"copy:deploy": "cp -r dist deploy && cp CNAME deploy",
|
|
33
|
-
"lint": "eslint --ext .js,.ts src"
|
|
34
|
-
},
|
|
35
26
|
"devDependencies": {
|
|
36
27
|
"@types/marked": "^4.0.3"
|
|
37
28
|
},
|
|
@@ -39,5 +30,15 @@
|
|
|
39
30
|
"hanabi": "^0.4.0",
|
|
40
31
|
"insane": "^2.6.2",
|
|
41
32
|
"marked": "^4.0.14"
|
|
33
|
+
},
|
|
34
|
+
"scripts": {
|
|
35
|
+
"dev": "vite",
|
|
36
|
+
"build": "pnpm lint && vite build",
|
|
37
|
+
"build:demo": "vite build --config vite-demo.config.ts",
|
|
38
|
+
"serve": "vite preview",
|
|
39
|
+
"deploy": "gh-pages -d deploy",
|
|
40
|
+
"predeploy": "pnpm build && pnpm build:demo && pnpm copy:deploy",
|
|
41
|
+
"copy:deploy": "cp -r dist deploy && cp CNAME deploy",
|
|
42
|
+
"lint": "eslint --ext .js,.ts src"
|
|
42
43
|
}
|
|
43
|
-
}
|
|
44
|
+
}
|
package/src/api/index.ts
CHANGED
|
@@ -117,6 +117,12 @@ export default class Api {
|
|
|
117
117
|
}
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
+
/** 用户 · 登录状态 */
|
|
121
|
+
public async loginStatus() {
|
|
122
|
+
const data = await GET<any>(this.ctx, `${this.baseURL}/login-status`)
|
|
123
|
+
return (data || { is_login: false }) as { is_login: boolean }
|
|
124
|
+
}
|
|
125
|
+
|
|
120
126
|
// ============================
|
|
121
127
|
// 页面 Page
|
|
122
128
|
// ============================
|
|
@@ -264,6 +270,29 @@ export default class Api {
|
|
|
264
270
|
return p.pv as number
|
|
265
271
|
}
|
|
266
272
|
|
|
273
|
+
/** 图片上传 */
|
|
274
|
+
public async imgUpload(file: File) {
|
|
275
|
+
const params: any = {
|
|
276
|
+
name: this.ctx.user.data.nick,
|
|
277
|
+
email: this.ctx.user.data.email,
|
|
278
|
+
page_key: this.ctx.conf.pageKey,
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (this.ctx.conf.site) params.site_name = this.ctx.conf.site
|
|
282
|
+
|
|
283
|
+
const form = ToFormData(params)
|
|
284
|
+
form.set('file', file)
|
|
285
|
+
|
|
286
|
+
const init: RequestInit = {
|
|
287
|
+
method: 'POST',
|
|
288
|
+
body: form
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const json = await Fetch(this.ctx, `${this.baseURL}/img-upload`, init)
|
|
292
|
+
return ((json.data || {}) as any) as { img_file: string, img_url: string }
|
|
293
|
+
|
|
294
|
+
}
|
|
295
|
+
|
|
267
296
|
// ============================
|
|
268
297
|
// 验证码
|
|
269
298
|
// ============================
|
|
@@ -279,4 +308,10 @@ export default class Api {
|
|
|
279
308
|
const data = await GET<any>(this.ctx, `${this.baseURL}/captcha/check`, { value })
|
|
280
309
|
return (data.img_data || '') as string
|
|
281
310
|
}
|
|
311
|
+
|
|
312
|
+
/** 验证码 · 状态 */
|
|
313
|
+
public async captchaStatus() {
|
|
314
|
+
const data = await GET<any>(this.ctx, `${this.baseURL}/captcha/status`)
|
|
315
|
+
return (data || { is_pass: false }) as { is_pass: boolean }
|
|
316
|
+
}
|
|
282
317
|
}
|
package/src/api/request.ts
CHANGED
|
@@ -43,6 +43,7 @@ export async function Fetch(ctx: Context, input: RequestInfo, init: RequestInit,
|
|
|
43
43
|
json = await (new Promise<any>((resolve, reject) => {
|
|
44
44
|
ctx.trigger('checker-captcha', {
|
|
45
45
|
imgData: json.data.img_data,
|
|
46
|
+
iframe: json.data.iframe,
|
|
46
47
|
onSuccess: () => {
|
|
47
48
|
recall(resolve, reject)
|
|
48
49
|
},
|
package/src/artalk.ts
CHANGED
|
@@ -12,6 +12,7 @@ import SidebarLayer from './components/sidebar-layer'
|
|
|
12
12
|
import { GetLayerWrap } from './components/layer'
|
|
13
13
|
import { EventPayloadMap, Handler } from '~/types/event'
|
|
14
14
|
import Api from './api'
|
|
15
|
+
import * as Utils from './lib/utils'
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
18
|
* Artalk
|
|
@@ -59,6 +60,7 @@ export default class Artalk {
|
|
|
59
60
|
|
|
60
61
|
// 组件初始化
|
|
61
62
|
this.checkerLauncher = new CheckerLauncher(this.ctx)
|
|
63
|
+
Utils.initMarked(this.ctx) // 初始化 marked
|
|
62
64
|
|
|
63
65
|
// 编辑器
|
|
64
66
|
this.editor = new Editor(this.ctx)
|
|
@@ -80,6 +82,13 @@ export default class Artalk {
|
|
|
80
82
|
|
|
81
83
|
// 其他
|
|
82
84
|
this.initPV()
|
|
85
|
+
|
|
86
|
+
// 插件初始化
|
|
87
|
+
Artalk.Plugins.forEach(plugin => {
|
|
88
|
+
if (typeof plugin === 'function') {
|
|
89
|
+
plugin(this.ctx)
|
|
90
|
+
}
|
|
91
|
+
})
|
|
83
92
|
}
|
|
84
93
|
|
|
85
94
|
/** 事件绑定 · 初始化 */
|
|
@@ -148,6 +157,7 @@ export default class Artalk {
|
|
|
148
157
|
/** 暗黑模式 · 设定 */
|
|
149
158
|
public setDarkMode(darkMode: boolean) {
|
|
150
159
|
this.ctx.conf.darkMode = darkMode
|
|
160
|
+
this.ctx.trigger('conf-updated')
|
|
151
161
|
this.initDarkMode()
|
|
152
162
|
}
|
|
153
163
|
|
|
@@ -179,4 +189,12 @@ export default class Artalk {
|
|
|
179
189
|
public trigger<K extends keyof EventPayloadMap>(name: K, payload?: EventPayloadMap[K]) {
|
|
180
190
|
this.ctx.trigger(name, payload, 'external')
|
|
181
191
|
}
|
|
192
|
+
|
|
193
|
+
/** Plugins */
|
|
194
|
+
protected static Plugins: ((ctx: Context) => void)[] = []
|
|
195
|
+
|
|
196
|
+
/** Enable Plugin */
|
|
197
|
+
public static Use(plugin: ((ctx: Context) => void)) {
|
|
198
|
+
this.Plugins.push(plugin)
|
|
199
|
+
}
|
|
182
200
|
}
|
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.initImgUpload()
|
|
247
251
|
}
|
|
248
252
|
|
|
249
253
|
/** 关闭编辑器插件 */
|
|
@@ -253,6 +257,116 @@ 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
|
+
initImgUpload() {
|
|
265
|
+
this.$imgUploadBtn = Utils.createElement(`<span class="atk-plug-btn">图片</span>`)
|
|
266
|
+
this.$plugBtnWrap.querySelector('[data-plug-name="preview"]')!.before(this.$imgUploadBtn) // 显示在预览图标之前
|
|
267
|
+
|
|
268
|
+
// 按钮点击
|
|
269
|
+
this.$imgUploadBtn.onclick = () => {
|
|
270
|
+
// 选择图片
|
|
271
|
+
const $input = document.createElement('input')
|
|
272
|
+
$input.type = 'file'
|
|
273
|
+
$input.accept = this.allowImgExts.map(o => `.${o}`).join(',')
|
|
274
|
+
$input.onchange = () => {
|
|
275
|
+
(async () => { // 解决阻塞 UI 问题
|
|
276
|
+
if (!$input.files || $input.files.length === 0) return
|
|
277
|
+
const file = $input.files[0]
|
|
278
|
+
this.uploadImg(file)
|
|
279
|
+
})()
|
|
280
|
+
}
|
|
281
|
+
$input.click() // 显示选择图片对话框
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// 统一从 FileList 获取文件并上传图片方法
|
|
285
|
+
const uploadFromFileList = (files?: FileList) => {
|
|
286
|
+
if (!files) return
|
|
287
|
+
|
|
288
|
+
for (let i = 0; i < files.length; i++) {
|
|
289
|
+
const file = files[i]
|
|
290
|
+
this.uploadImg(file)
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// 拖拽图片
|
|
295
|
+
// @link https://developer.mozilla.org/zh-CN/docs/Web/API/HTML_Drag_and_Drop_API/File_drag_and_drop
|
|
296
|
+
// 阻止浏览器的默认释放行为
|
|
297
|
+
this.$textarea.addEventListener('dragover', (evt) => {
|
|
298
|
+
evt.stopPropagation()
|
|
299
|
+
evt.preventDefault()
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
this.$textarea.addEventListener('drop', (evt) => {
|
|
303
|
+
const files = evt.dataTransfer?.files
|
|
304
|
+
if (files?.length) {
|
|
305
|
+
evt.preventDefault()
|
|
306
|
+
uploadFromFileList(files)
|
|
307
|
+
}
|
|
308
|
+
})
|
|
309
|
+
|
|
310
|
+
// 粘贴图片
|
|
311
|
+
this.$textarea.addEventListener('paste', (evt) => {
|
|
312
|
+
const files = evt.clipboardData?.files
|
|
313
|
+
if (files?.length) {
|
|
314
|
+
evt.preventDefault()
|
|
315
|
+
uploadFromFileList(files)
|
|
316
|
+
}
|
|
317
|
+
})
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** 刷新图片上传按钮 */
|
|
321
|
+
refreshUploadBtn() {
|
|
322
|
+
if (!this.$imgUploadBtn) return
|
|
323
|
+
|
|
324
|
+
if (!this.ctx.conf.imgUpload) {
|
|
325
|
+
this.$imgUploadBtn.setAttribute('atk-only-admin-show', '')
|
|
326
|
+
this.ctx.trigger('check-admin-show-el')
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
async uploadImg(file: File) {
|
|
331
|
+
const fileExt = /[^.]+$/.exec(file.name)
|
|
332
|
+
if (!fileExt || !this.allowImgExts.includes(fileExt[0])) return
|
|
333
|
+
|
|
334
|
+
// 未登录提示
|
|
335
|
+
if (!this.ctx.user.checkHasBasicUserInfo()) {
|
|
336
|
+
this.showNotify('填入你的名字邮箱才能上传哦', 'w')
|
|
337
|
+
return
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// 插入图片前换一行
|
|
341
|
+
let insertPrefix = '\n'
|
|
342
|
+
if (this.$textarea.value.trim() === '') insertPrefix = ''
|
|
343
|
+
|
|
344
|
+
// 插入占位加载文字
|
|
345
|
+
const uploadPlaceholderTxt = `${insertPrefix}`
|
|
346
|
+
this.insertContent(uploadPlaceholderTxt)
|
|
347
|
+
|
|
348
|
+
// 上传图片
|
|
349
|
+
let resp: any
|
|
350
|
+
try {
|
|
351
|
+
resp = await new Api(this.ctx).imgUpload(file)
|
|
352
|
+
} catch (err: any) {
|
|
353
|
+
console.error(err)
|
|
354
|
+
this.showNotify(`图片上传失败,${err.msg}`, 'e')
|
|
355
|
+
}
|
|
356
|
+
if (!!resp && resp.img_url) {
|
|
357
|
+
let imgURL = resp.img_url as string
|
|
358
|
+
|
|
359
|
+
// 若为相对路径,加上 artalk server
|
|
360
|
+
if (!Utils.isValidURL(imgURL)) imgURL = Utils.getURLBasedOnApi(this.ctx, imgURL)
|
|
361
|
+
|
|
362
|
+
// 上传成功插入图片
|
|
363
|
+
this.setContent(this.$textarea.value.replace(uploadPlaceholderTxt, `${insertPrefix}`))
|
|
364
|
+
} else {
|
|
365
|
+
// 上传失败删除加载文字
|
|
366
|
+
this.setContent(this.$textarea.value.replace(uploadPlaceholderTxt, ''))
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
256
370
|
insertContent (val: string) {
|
|
257
371
|
if ((document as any).selection) {
|
|
258
372
|
this.$textarea.focus();
|
|
@@ -287,7 +401,8 @@ export default class Editor extends Component {
|
|
|
287
401
|
this.cancelReply()
|
|
288
402
|
}
|
|
289
403
|
|
|
290
|
-
|
|
404
|
+
/** 获取最终用于 submit 的数据 */
|
|
405
|
+
getFinalContent () {
|
|
291
406
|
let content = this.getContentOriginal()
|
|
292
407
|
|
|
293
408
|
// 表情包处理
|
|
@@ -304,7 +419,7 @@ export default class Editor extends Component {
|
|
|
304
419
|
}
|
|
305
420
|
|
|
306
421
|
getContentMarked () {
|
|
307
|
-
return Utils.marked(this.ctx, this.
|
|
422
|
+
return Utils.marked(this.ctx, this.getFinalContent())
|
|
308
423
|
}
|
|
309
424
|
|
|
310
425
|
initBottomPart () {
|
|
@@ -363,7 +478,7 @@ export default class Editor extends Component {
|
|
|
363
478
|
}
|
|
364
479
|
|
|
365
480
|
async submit () {
|
|
366
|
-
if (this.
|
|
481
|
+
if (this.getFinalContent().trim() === '') {
|
|
367
482
|
this.$textarea.focus()
|
|
368
483
|
return
|
|
369
484
|
}
|
|
@@ -374,7 +489,7 @@ export default class Editor extends Component {
|
|
|
374
489
|
|
|
375
490
|
try {
|
|
376
491
|
const nComment = await new Api(this.ctx).add({
|
|
377
|
-
content: this.
|
|
492
|
+
content: this.getFinalContent(),
|
|
378
493
|
nick: this.user.data.nick,
|
|
379
494
|
email: this.user.data.email,
|
|
380
495
|
link: this.user.data.link,
|
package/src/components/layer.ts
CHANGED
|
@@ -65,12 +65,7 @@ export default class Layer extends Component {
|
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
// body style 禁止滚动 + 防抖
|
|
68
|
-
this.
|
|
69
|
-
this.bodyStyleOrgPaddingRight = document.body.style.paddingRight
|
|
70
|
-
document.body.style.overflow = 'hidden'
|
|
71
|
-
|
|
72
|
-
const bpr = parseInt(window.getComputedStyle(document.body, null).getPropertyValue('padding-right'), 10)
|
|
73
|
-
document.body.style.paddingRight = `${Ui.getScrollBarWidth() + bpr || 0}px`
|
|
68
|
+
this.pageBodyScrollBarHide()
|
|
74
69
|
}
|
|
75
70
|
|
|
76
71
|
hide () {
|
|
@@ -81,8 +76,7 @@ export default class Layer extends Component {
|
|
|
81
76
|
this.newActionTimer(() => {
|
|
82
77
|
this.$wrap.style.display = 'none'
|
|
83
78
|
// body style 禁止滚动解除
|
|
84
|
-
|
|
85
|
-
document.body.style.paddingRight = this.bodyStyleOrgPaddingRight
|
|
79
|
+
this.pageBodyScrollBarShow()
|
|
86
80
|
this.checkCleanLayer()
|
|
87
81
|
}, 450)
|
|
88
82
|
this.newActionTimer(() => {
|
|
@@ -95,6 +89,22 @@ export default class Layer extends Component {
|
|
|
95
89
|
this.maskClickHideEnable = enable
|
|
96
90
|
}
|
|
97
91
|
|
|
92
|
+
// 页面滚动条隐藏
|
|
93
|
+
pageBodyScrollBarHide() {
|
|
94
|
+
this.bodyStyleOrgOverflow = document.body.style.overflow
|
|
95
|
+
this.bodyStyleOrgPaddingRight = document.body.style.paddingRight
|
|
96
|
+
document.body.style.overflow = 'hidden'
|
|
97
|
+
|
|
98
|
+
const bpr = parseInt(window.getComputedStyle(document.body, null).getPropertyValue('padding-right'), 10)
|
|
99
|
+
document.body.style.paddingRight = `${Ui.getScrollBarWidth() + bpr || 0}px`
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// 页面滚动条显示
|
|
103
|
+
pageBodyScrollBarShow() {
|
|
104
|
+
document.body.style.overflow = this.bodyStyleOrgOverflow
|
|
105
|
+
document.body.style.paddingRight = this.bodyStyleOrgPaddingRight
|
|
106
|
+
}
|
|
107
|
+
|
|
98
108
|
// Timers
|
|
99
109
|
private static actionTimers: {act: Function, tid: number}[] = []
|
|
100
110
|
|
|
@@ -120,6 +130,7 @@ export default class Layer extends Component {
|
|
|
120
130
|
disposeNow () {
|
|
121
131
|
document.body.style.overflow = ''
|
|
122
132
|
this.$el.remove()
|
|
133
|
+
this.pageBodyScrollBarShow()
|
|
123
134
|
// this.$el dispose
|
|
124
135
|
this.checkCleanLayer()
|
|
125
136
|
}
|
|
@@ -7,6 +7,7 @@ import Comment from './comment'
|
|
|
7
7
|
import { ListData, CommentData, NotifyData, ApiVersionData } from '~/types/artalk-data'
|
|
8
8
|
import Pagination from './pagination'
|
|
9
9
|
import ReadMoreBtn from './read-more-btn'
|
|
10
|
+
import { backendMinVersion } from '../../package.json'
|
|
10
11
|
|
|
11
12
|
export default class ListLite extends Component {
|
|
12
13
|
protected $commentsWrap: HTMLElement
|
|
@@ -117,7 +118,14 @@ export default class ListLite extends Component {
|
|
|
117
118
|
this.data = data
|
|
118
119
|
|
|
119
120
|
// 版本检测
|
|
120
|
-
|
|
121
|
+
const feMinVersion = data.api_version?.fe_min_version || '0.0.0'
|
|
122
|
+
if (this.ctx.conf.versionCheck && this.versionCheck('前端', feMinVersion, ARTALK_VERSION)) return
|
|
123
|
+
if (this.ctx.conf.versionCheck && this.versionCheck('后端', backendMinVersion, data.api_version?.version)) return
|
|
124
|
+
|
|
125
|
+
// 图片上传功能
|
|
126
|
+
if (data.conf && typeof data.conf.img_upload === "boolean") {
|
|
127
|
+
this.ctx.conf.imgUpload = data.conf.img_upload
|
|
128
|
+
}
|
|
121
129
|
|
|
122
130
|
// 导入数据
|
|
123
131
|
this.importComments(data.comments)
|
|
@@ -130,6 +138,7 @@ export default class ListLite extends Component {
|
|
|
130
138
|
|
|
131
139
|
this.ctx.trigger('unread-update', { notifies: data.unread || [] })
|
|
132
140
|
this.ctx.trigger('comments-loaded')
|
|
141
|
+
this.ctx.trigger('conf-updated')
|
|
133
142
|
|
|
134
143
|
if (this.onAfterLoad) this.onAfterLoad(data)
|
|
135
144
|
}
|
|
@@ -466,25 +475,25 @@ export default class ListLite extends Component {
|
|
|
466
475
|
}
|
|
467
476
|
}
|
|
468
477
|
|
|
469
|
-
/**
|
|
470
|
-
public versionCheck(
|
|
471
|
-
const
|
|
472
|
-
const needUpdate = Utils.versionCompare(needVersion, ARTALK_VERSION) === 1
|
|
478
|
+
/** 版本检测 */
|
|
479
|
+
public versionCheck(name: '前端'|'后端', needVersion: string, curtVersion: string): boolean {
|
|
480
|
+
const needUpdate = Utils.versionCompare(needVersion, curtVersion) === 1
|
|
473
481
|
if (needUpdate) {
|
|
474
482
|
// 需要更新
|
|
475
|
-
const errEl = Utils.createElement(`<div
|
|
476
|
-
+
|
|
483
|
+
const errEl = Utils.createElement(`<div>Artalk ${name}版本已过时,请更新以获得完整体验<br/>`
|
|
484
|
+
+ `如果你是管理员,请前往 “<a href="https://artalk.js.org/" target="_blank">官方文档</a>” 获得帮助`
|
|
477
485
|
+ `<br/><br/>`
|
|
478
|
-
+ `<span style="color: var(--at-color-meta);"
|
|
486
|
+
+ `<span style="color: var(--at-color-meta);">当前${name}版本 ${curtVersion},需求版本 >= ${needVersion}</span><br/><br/>`
|
|
479
487
|
+ `</div>`)
|
|
480
488
|
const ignoreBtn = Utils.createElement('<span style="cursor:pointer;">忽略</span>')
|
|
481
489
|
ignoreBtn.onclick = () => {
|
|
482
|
-
Ui.setError(this.
|
|
490
|
+
Ui.setError(this.$el.parentElement!, null)
|
|
483
491
|
this.ctx.conf.versionCheck = false
|
|
492
|
+
this.ctx.trigger('conf-updated')
|
|
484
493
|
this.fetchComments(0)
|
|
485
494
|
}
|
|
486
495
|
errEl.append(ignoreBtn)
|
|
487
|
-
Ui.setError(this.
|
|
496
|
+
Ui.setError(this.$el.parentElement!, errEl, '<span class="atk-warn-title">Artalk Warn</span>')
|
|
488
497
|
}
|
|
489
498
|
|
|
490
499
|
return needUpdate
|
package/src/components/list.ts
CHANGED
|
@@ -42,9 +42,12 @@ export default class List extends ListLite {
|
|
|
42
42
|
// 操作按钮
|
|
43
43
|
this.initListActionBtn()
|
|
44
44
|
|
|
45
|
-
// 下拉选择层
|
|
46
45
|
this.$commentCount = this.$el.querySelector('.atk-comment-count')!
|
|
47
|
-
|
|
46
|
+
|
|
47
|
+
// 评论列表排序 Dropdown 下拉选择层
|
|
48
|
+
if (this.ctx.conf.listSort) {
|
|
49
|
+
this.initDropdown()
|
|
50
|
+
}
|
|
48
51
|
|
|
49
52
|
// copyright
|
|
50
53
|
this.$el.querySelector<HTMLElement>('.atk-copyright')!.innerHTML = `Powered By <a href="https://artalk.js.org" target="_blank" title="Artalk v${ARTALK_VERSION}">Artalk</a>`
|
|
@@ -184,7 +187,7 @@ export default class List extends ListLite {
|
|
|
184
187
|
this.$commentCount.classList.add('atk-dropdown-wrap')
|
|
185
188
|
|
|
186
189
|
// 插入图标
|
|
187
|
-
this.$
|
|
190
|
+
this.$dropdownWrap.append(Utils.createElement(`<span class="atk-arrow-down-icon"></span>`))
|
|
188
191
|
|
|
189
192
|
const reloadUseParamsEditor = (func: (p: any) => void) => {
|
|
190
193
|
this.paramsEditor = (p) => { func(p) }
|
|
@@ -6,6 +6,7 @@ import * as Utils from '@/lib/utils'
|
|
|
6
6
|
import SidebarHTML from './html/sidebar-layer.html?raw'
|
|
7
7
|
import * as Ui from '@/lib/ui'
|
|
8
8
|
import Layer from './layer'
|
|
9
|
+
import Api from '../api'
|
|
9
10
|
|
|
10
11
|
export default class SidebarLayer extends Component {
|
|
11
12
|
public layer?: Layer
|
|
@@ -60,18 +61,39 @@ export default class SidebarLayer extends Component {
|
|
|
60
61
|
|
|
61
62
|
// 第一次加载
|
|
62
63
|
if (this.firstShow) {
|
|
64
|
+
// 管理员身份验证
|
|
65
|
+
if (this.ctx.user.data.isAdmin) {
|
|
66
|
+
const resp = await new Api(this.ctx).loginStatus()
|
|
67
|
+
if (!resp.is_login) {
|
|
68
|
+
await (new Promise<any>((resolve, reject) => {
|
|
69
|
+
this.ctx.trigger('checker-admin', {
|
|
70
|
+
onSuccess: () => {
|
|
71
|
+
resolve(null)
|
|
72
|
+
setTimeout(() => {
|
|
73
|
+
this.show()
|
|
74
|
+
}, 500)
|
|
75
|
+
},
|
|
76
|
+
onCancel: () => {
|
|
77
|
+
this.layer?.hide()
|
|
78
|
+
}
|
|
79
|
+
})
|
|
80
|
+
}))
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
63
84
|
this.$iframeWrap.innerHTML = ''
|
|
64
85
|
this.$iframe = Utils.createElement<HTMLIFrameElement>('<iframe></iframe>')
|
|
65
86
|
|
|
66
87
|
const baseURL = (import.meta.env.MODE === 'development') ? 'http://localhost:23367/'
|
|
67
|
-
:
|
|
88
|
+
: Utils.getURLBasedOnApi(this.ctx, 'sidebar/')
|
|
68
89
|
const userData = encodeURIComponent(JSON.stringify(this.ctx.user.data))
|
|
69
90
|
|
|
70
|
-
|
|
91
|
+
const location = window.location
|
|
71
92
|
this.iframeLoad(`${baseURL}`
|
|
72
93
|
+ `?pageKey=${encodeURIComponent(this.conf.pageKey)}`
|
|
73
94
|
+ `&site=${encodeURIComponent(this.conf.site || '')}`
|
|
74
95
|
+ `&user=${userData}`
|
|
96
|
+
+ `&referer=${encodeURIComponent(`${location.protocol}//${location.host}${location.pathname}`)}`
|
|
75
97
|
+ `${((this.conf.darkMode) ? `&darkMode=1` : ``)}`)
|
|
76
98
|
|
|
77
99
|
this.$iframeWrap.append(this.$iframe)
|
package/src/context.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { marked as libMarked } from 'marked'
|
|
1
2
|
import User from './lib/user'
|
|
2
3
|
import ArtalkConfig from '~/types/artalk-config'
|
|
3
4
|
import { EventPayloadMap, Event, EventScopeType, Handler } from '~/types/event'
|
|
@@ -39,4 +40,6 @@ export default class Context {
|
|
|
39
40
|
.map((evt) => evt.handler)
|
|
40
41
|
.forEach((handler) => handler(payload as any))
|
|
41
42
|
}
|
|
43
|
+
|
|
44
|
+
public markedInstance!: typeof libMarked
|
|
42
45
|
}
|
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
|
+
listSort: true,
|
|
20
21
|
pvEl: '#ArtalkPV',
|
|
21
22
|
flatMode: 'auto',
|
|
22
23
|
maxNesting: 3,
|
|
@@ -36,6 +37,7 @@ const defaults: ArtalkConfig = {
|
|
|
36
37
|
children: 400,
|
|
37
38
|
},
|
|
38
39
|
|
|
40
|
+
imgUpload: true,
|
|
39
41
|
reqTimeout: 15000,
|
|
40
42
|
versionCheck: true,
|
|
41
43
|
}
|
|
@@ -5,7 +5,7 @@ import { Checker } from '.'
|
|
|
5
5
|
const AdminChecker: Checker = {
|
|
6
6
|
inputType: 'password',
|
|
7
7
|
|
|
8
|
-
request(that, inputVal) {
|
|
8
|
+
request(that, ctx, inputVal) {
|
|
9
9
|
const data = {
|
|
10
10
|
name: that.ctx.user.data.nick,
|
|
11
11
|
email: that.ctx.user.data.email,
|
|
@@ -15,11 +15,11 @@ const AdminChecker: Checker = {
|
|
|
15
15
|
return new Api(that.ctx).login(data.name, data.email, data.password)
|
|
16
16
|
},
|
|
17
17
|
|
|
18
|
-
body() {
|
|
18
|
+
body(that, ctx) {
|
|
19
19
|
return Utils.createElement('<span>敲入密码来验证管理员身份:</span>')
|
|
20
20
|
},
|
|
21
21
|
|
|
22
|
-
onSuccess(that, userToken, inputVal, formEl) {
|
|
22
|
+
onSuccess(that, ctx, userToken, inputVal, formEl) {
|
|
23
23
|
that.ctx.user.data.isAdmin = true
|
|
24
24
|
that.ctx.user.data.token = userToken
|
|
25
25
|
that.ctx.user.save()
|
|
@@ -27,7 +27,7 @@ const AdminChecker: Checker = {
|
|
|
27
27
|
that.ctx.trigger('list-reload')
|
|
28
28
|
},
|
|
29
29
|
|
|
30
|
-
onError(that, err, inputVal, formEl) {}
|
|
30
|
+
onError(that, ctx, err, inputVal, formEl) {}
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
export default AdminChecker
|
|
@@ -1,15 +1,61 @@
|
|
|
1
1
|
import Api from '@/api'
|
|
2
2
|
import * as Utils from '../utils'
|
|
3
|
+
import * as Ui from '../ui'
|
|
3
4
|
import { Checker } from '.'
|
|
4
5
|
|
|
5
6
|
const CaptchaChecker: Checker = {
|
|
6
|
-
request(that, inputVal) {
|
|
7
|
+
request(that, ctx, inputVal) {
|
|
7
8
|
return new Api(that.ctx).captchaCheck(inputVal)
|
|
8
9
|
},
|
|
9
10
|
|
|
10
|
-
body(that) {
|
|
11
|
+
body(that, ctx) {
|
|
12
|
+
// Iframe 验证方式
|
|
13
|
+
if (that.captchaConf.iframe) {
|
|
14
|
+
const $iframeWrap = Utils.createElement(`<div class="atk-checker-iframe-wrap"></div>`)
|
|
15
|
+
const $iframe = Utils.createElement<HTMLIFrameElement>(`<iframe class="atk-fade-in"></iframe>`)
|
|
16
|
+
$iframe.style.display = 'none'
|
|
17
|
+
Ui.showLoading($iframeWrap, { transparentBg: true })
|
|
18
|
+
$iframe.src = `${that.ctx.conf.server}/captcha/get`
|
|
19
|
+
$iframe.onload = () => {
|
|
20
|
+
$iframe.style.display = ''
|
|
21
|
+
Ui.hideLoading($iframeWrap)
|
|
22
|
+
}
|
|
23
|
+
$iframeWrap.append($iframe)
|
|
24
|
+
|
|
25
|
+
const $closeBtn = Utils.createElement(`<div class="atk-close-btn"><i class="atk-icon atk-icon-close"></i></div>`)
|
|
26
|
+
$iframeWrap.append($closeBtn)
|
|
27
|
+
|
|
28
|
+
ctx.hideInteractInput()
|
|
29
|
+
|
|
30
|
+
// 轮询状态
|
|
31
|
+
let stop = false // 打断
|
|
32
|
+
const sleep = (ms: number) => new Promise((resolve) => { window.setTimeout(() => { resolve(null) }, ms) })
|
|
33
|
+
;(async function queryStatus() {
|
|
34
|
+
await sleep(1000)
|
|
35
|
+
if (stop) return
|
|
36
|
+
let isPass = false
|
|
37
|
+
try {
|
|
38
|
+
const resp = await new Api(that.ctx).captchaStatus()
|
|
39
|
+
isPass = resp.is_pass
|
|
40
|
+
} catch { isPass = false }
|
|
41
|
+
if (isPass) {
|
|
42
|
+
ctx.triggerSuccess()
|
|
43
|
+
} else {
|
|
44
|
+
queryStatus()
|
|
45
|
+
}
|
|
46
|
+
})()
|
|
47
|
+
|
|
48
|
+
$closeBtn.onclick = () => {
|
|
49
|
+
stop = true
|
|
50
|
+
ctx.cancel()
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return $iframeWrap
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// 图片验证方式
|
|
11
57
|
const elem = Utils.createElement(
|
|
12
|
-
`<span><img class="atk-captcha-img" src="${that.
|
|
58
|
+
`<span><img class="atk-captcha-img" src="${that.captchaConf.imgData || ''}" alt="验证码">敲入验证码继续:</span>`
|
|
13
59
|
);
|
|
14
60
|
|
|
15
61
|
// 刷新验证码
|
|
@@ -26,11 +72,11 @@ const CaptchaChecker: Checker = {
|
|
|
26
72
|
return elem
|
|
27
73
|
},
|
|
28
74
|
|
|
29
|
-
onSuccess(that, data, inputVal, formEl) {
|
|
30
|
-
that.
|
|
75
|
+
onSuccess(that, ctx, data, inputVal, formEl) {
|
|
76
|
+
that.captchaConf.val = inputVal
|
|
31
77
|
},
|
|
32
78
|
|
|
33
|
-
onError(that, err, inputVal, formEl) {
|
|
79
|
+
onError(that, ctx, err, inputVal, formEl) {
|
|
34
80
|
formEl.querySelector<HTMLElement>('.atk-captcha-img')!.click() // 刷新验证码
|
|
35
81
|
}
|
|
36
82
|
}
|