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/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "artalk",
3
- "version": "2.1.7",
4
- "license": "GPL-3.0",
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
  }
@@ -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
  }
@@ -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}![](Uploading ${file.name}...)`
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}![](${imgURL})`))
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
- getContent () {
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.getContent())
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.getContent().trim() === '') {
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.getContent(),
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,
@@ -65,12 +65,7 @@ export default class Layer extends Component {
65
65
  }
66
66
 
67
67
  // body style 禁止滚动 + 防抖
68
- this.bodyStyleOrgOverflow = document.body.style.overflow
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
- document.body.style.overflow = this.bodyStyleOrgOverflow
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
- if (this.ctx.conf.versionCheck && this.versionCheck(data.api_version)) return
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(versionData: ApiVersionData): boolean {
471
- const needVersion = versionData?.fe_min_version || '0.0.0'
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>前端 Artalk 版本已过时,请更新以获得完整体验<br/>`
476
- + `若您是站点管理员,请前往 “<a href="https://artalk.js.org/" target="_blank">官方文档</a>” 获取帮助`
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);">前端版本 ${ARTALK_VERSION},需求版本 >= ${needVersion}</span><br/><br/>`
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.ctx, null)
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.ctx, errEl, '<span class="atk-warn-title">Artalk Warn</span>')
496
+ Ui.setError(this.$el.parentElement!, errEl, '<span class="atk-warn-title">Artalk Warn</span>')
488
497
  }
489
498
 
490
499
  return needUpdate
@@ -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
- this.initDropdown()
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.$commentsWrap.append(Utils.createElement(`<span class="atk-arrow-down-icon"></span>`))
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
- : `${this.conf.server.replace(/\/$/, '')}/../sidebar/`
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.submitCaptchaImgData || ''}" alt="验证码">敲入验证码继续:</span>`
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.submitCaptchaVal = inputVal
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
  }