artalk 2.2.11 → 2.2.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.html CHANGED
@@ -38,7 +38,9 @@
38
38
  <script src="https://unpkg.com/vconsole@latest/dist/vconsole.min.js"></script>
39
39
  <script>
40
40
  // VConsole will be exported to `window.VConsole` by default.
41
- var vConsole = new window.VConsole();
41
+ if (navigator.userAgentData.mobile) {
42
+ var vConsole = new window.VConsole();
43
+ }
42
44
  </script>
43
45
  </head>
44
46
  <body>
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "artalk",
3
- "version": "2.2.11",
4
- "backendMinVersion": "2.1.4",
3
+ "version": "2.2.12",
4
+ "backendMinVersion": "2.1.6",
5
5
  "license": "LGPL-3.0",
6
6
  "description": "A Selfhosted Comment System",
7
7
  "keywords": [
@@ -29,7 +29,8 @@
29
29
  "dependencies": {
30
30
  "hanabi": "^0.4.0",
31
31
  "insane": "^2.6.2",
32
- "marked": "^4.0.14"
32
+ "marked": "^4.0.14",
33
+ "abortcontroller-polyfill": "^1.7.3"
33
34
  },
34
35
  "scripts": {
35
36
  "dev": "vite",
package/src/api/index.ts CHANGED
@@ -3,6 +3,7 @@ import ArtalkConfig from '~/types/artalk-config'
3
3
  import Context from '../context'
4
4
  import { Fetch, ToFormData, POST, GET } from './request'
5
5
  import * as Utils from '../lib/utils'
6
+ import 'abortcontroller-polyfill/dist/polyfill-patch-fetch'
6
7
 
7
8
  export default class Api {
8
9
  private ctx: Context
@@ -95,8 +96,6 @@ export default class Api {
95
96
  /** 用户 · 获取 */
96
97
  public userGet(name: string, email: string) {
97
98
  const ctrl = new AbortController()
98
- const { signal } = ctrl
99
-
100
99
  const params: any = {
101
100
  name, email, site_name: (this.ctx.conf.site || '')
102
101
  }
@@ -104,7 +103,7 @@ export default class Api {
104
103
  const req = Fetch(this.ctx, `${this.baseURL}/user-get`, {
105
104
  method: 'POST',
106
105
  body: ToFormData(params),
107
- signal,
106
+ signal: ctrl.signal,
108
107
  }).then((json) => ({
109
108
  user: json.data.user as UserData|null,
110
109
  is_login: json.data.is_login as boolean,
@@ -307,6 +306,8 @@ export default class Api {
307
306
  conf.emoticons = conf.emoticons.trim()
308
307
  if (conf.emoticons.startsWith("[") || conf.emoticons.startsWith("{")) {
309
308
  conf.emoticons = JSON.parse(conf.emoticons) // pase json
309
+ } else if (conf.emoticons === "false") {
310
+ conf.emoticons = false
310
311
  }
311
312
  }
312
313
 
@@ -4,81 +4,55 @@ import Context from '../context'
4
4
  export async function Fetch(ctx: Context, input: RequestInfo, init: RequestInit, timeout?: number): Promise<any> {
5
5
  // JWT
6
6
  if (ctx.user.data.token) {
7
- const requestHeaders: HeadersInit = new Headers()
8
- requestHeaders.set('Authorization', `Bearer ${ctx.user.data.token}`)
9
-
10
- init.headers = requestHeaders
7
+ const headers = new Headers(init.headers) // 保留原有 headers
8
+ headers.set('Authorization', `Bearer ${ctx.user.data.token}`)
9
+ init.headers = headers
11
10
  }
12
11
 
13
12
  // 请求操作
14
- try {
13
+ const resp = await timeoutFetch(input, timeout || ctx.conf.reqTimeout || 15000, init)
15
14
 
16
- let resp: Response
17
- if ((typeof timeout !== 'number' && ctx.conf.reqTimeout === 0) || timeout === 0) {
18
- resp = await fetch(input, init)
19
- } else {
20
- // 请求超时检测
21
- resp = await timeoutPromise(timeout || ctx.conf.reqTimeout || 15000, fetch(input, init))
22
- }
15
+ const respHttpCode = resp.status
16
+ const noAccessCodes = [401, 400]
17
+ const isNoAccess = noAccessCodes.includes(respHttpCode)
23
18
 
24
- const noAccessCodes = [401, 400]
19
+ if (!resp.ok && !isNoAccess)
20
+ throw new Error(`请求响应 ${respHttpCode}`)
25
21
 
26
- if (!resp.ok && !noAccessCodes.includes(resp.status)) throw new Error(`请求响应 ${resp.status}`)
22
+ // 解析获取响应的 json
23
+ let json: any = await resp.json()
27
24
 
28
- // 解析获取响应的 json
29
- let json: any = await resp.json()
25
+ // 重新发起请求
26
+ const recall = (resolve, reject) => {
27
+ Fetch(ctx, input, init)
28
+ .then(d => { resolve(d) })
29
+ .catch(e => { reject(e) })
30
+ }
30
31
 
31
- // 重新发起请求
32
- const recall = (resolve, reject) => {
33
- Fetch(ctx, input, init).then(d => {
34
- resolve(d)
35
- }).catch(err => {
36
- reject(err)
32
+ // 请求弹出层验证
33
+ if (json.data?.need_captcha) {
34
+ // 请求需要验证码
35
+ json = await (new Promise<any>((resolve, reject) => {
36
+ ctx.trigger('checker-captcha', {
37
+ imgData: json.data.img_data,
38
+ iframe: json.data.iframe,
39
+ onSuccess: () => { recall(resolve, reject) },
40
+ onCancel: () => { reject(json) }
37
41
  })
38
- }
39
-
40
- // 请求弹出层验证
41
- if (json.data && json.data.need_captcha) {
42
- // 请求需要验证码
43
- json = await (new Promise<any>((resolve, reject) => {
44
- ctx.trigger('checker-captcha', {
45
- imgData: json.data.img_data,
46
- iframe: json.data.iframe,
47
- onSuccess: () => {
48
- recall(resolve, reject)
49
- },
50
- onCancel: () => {
51
- reject(json)
52
- }
53
- })
54
- }))
55
- } else if ((json.data && json.data.need_login) || noAccessCodes.includes(resp.status)) {
56
- // 请求需要管理员权限
57
- json = await (new Promise<any>((resolve, reject) => {
58
- ctx.trigger('checker-admin', {
59
- onSuccess: () => {
60
- recall(resolve, reject)
61
- },
62
- onCancel: () => {
63
- reject(json)
64
- }
65
- })
66
- }))
67
- }
68
-
69
- if (!json.success) throw json // throw 相当于 reject(json)
70
-
71
- return json
72
-
73
- } catch (err) {
74
- // 错误处理
75
- console.error(err)
42
+ }))
43
+ } else if (json.data?.need_login || isNoAccess) {
44
+ // 请求需要管理员权限
45
+ json = await (new Promise<any>((resolve, reject) => {
46
+ ctx.trigger('checker-admin', {
47
+ onSuccess: () => { recall(resolve, reject) },
48
+ onCancel: () => { reject(json) }
49
+ })
50
+ }))
51
+ }
76
52
 
77
- if (err instanceof TypeError)
78
- throw new Error(`网络错误`)
53
+ if (!json.success) throw json // throw 相当于 reject(json)
79
54
 
80
- throw err
81
- }
55
+ return json
82
56
  }
83
57
 
84
58
  /** 公共 POST 请求 */
@@ -108,21 +82,16 @@ export function ToFormData(object: {[key: string]: any}): FormData {
108
82
  }
109
83
 
110
84
  /** 我靠,fetch 一个 timeout,都要丑陋的实现 */
111
- function timeoutPromise<T>(ms: number, promise: Promise<T>): Promise<T> {
112
- return new Promise((resolve, reject) => {
113
- const timeoutId = setTimeout(() => {
114
- reject(new Error("请求超时"))
115
- }, ms)
116
-
117
- promise.then(
118
- (res) => {
119
- clearTimeout(timeoutId)
120
- resolve(res)
121
- },
122
- (err) => {
123
- clearTimeout(timeoutId)
124
- reject(err)
125
- }
126
- )
85
+ function timeoutFetch(url: RequestInfo, ms: number, opts: RequestInit) {
86
+ const controller = new AbortController()
87
+ opts.signal?.addEventListener('abort', () => controller.abort()) // 保留原有 signal 功能
88
+ let promise = fetch(url, { ...opts, signal: controller.signal })
89
+ if (ms > 0) {
90
+ const timer = setTimeout(() => controller.abort(), ms)
91
+ promise.finally(() => { clearTimeout(timer) })
92
+ }
93
+ promise = promise.catch((err) => {
94
+ throw ((err || {}).name === 'AbortError') ? new Error('请求超时或意外终止') : err
127
95
  })
96
+ return promise
128
97
  }
package/src/artalk.ts CHANGED
@@ -186,8 +186,23 @@ export default class Artalk {
186
186
 
187
187
  /** 暗黑模式 · 初始化 */
188
188
  public initDarkMode() {
189
+ if (this.conf.darkMode === 'auto') {
190
+ // 自动切换暗黑模式,事件监听
191
+ const darkModeMedia = window.matchMedia('(prefers-color-scheme: dark)')
192
+ darkModeMedia.addEventListener('change', (e) => { this.setDarkMode(e.matches) })
193
+ this.setDarkMode(darkModeMedia.matches)
194
+ } else {
195
+ this.setDarkMode(this.conf.darkMode || false)
196
+ }
197
+ }
198
+
199
+ /** 暗黑模式 · 设定 */
200
+ public setDarkMode(darkMode: boolean) {
189
201
  const darkModeClassName = 'atk-dark-mode'
190
202
 
203
+ this.ctx.conf.darkMode = darkMode
204
+ this.ctx.trigger('conf-updated')
205
+
191
206
  if (this.conf.darkMode) {
192
207
  this.$root.classList.add(darkModeClassName)
193
208
  } else {
@@ -205,13 +220,6 @@ export default class Artalk {
205
220
  }
206
221
  }
207
222
 
208
- /** 暗黑模式 · 设定 */
209
- public setDarkMode(darkMode: boolean) {
210
- this.ctx.conf.darkMode = darkMode
211
- this.ctx.trigger('conf-updated')
212
- this.initDarkMode()
213
- }
214
-
215
223
  /** PV */
216
224
  public async initPV() {
217
225
  const pvNum = await new Api(this.ctx).pv()
@@ -153,6 +153,7 @@ export default class Editor extends Component {
153
153
  this.getInputEl('link')!.value = data.user.link
154
154
  }
155
155
  })
156
+ .catch(() => {})
156
157
  .finally(() => {
157
158
  this.queryUserInfo.abortFunc = null // 清理
158
159
  })
@@ -213,22 +214,14 @@ export default class Editor extends Component {
213
214
  this.openedPlugName = null
214
215
  this.$plugBtnWrap.innerHTML = ''
215
216
 
216
- // 依次实例化 plug
217
+ // 初始化 plug 按钮
217
218
  this.LOADABLE_PLUG_LIST.forEach((PlugObj) => {
219
+ if (PlugObj.Name === 'emoticons' && !this.conf.emoticons) return
220
+
218
221
  // 切换按钮
219
222
  const btnElem = Utils.createElement(`<span class="atk-plug-btn" data-plug-name="${PlugObj.Name}">${PlugObj.BtnHTML}</span>`)
220
223
  this.$plugBtnWrap.appendChild(btnElem)
221
224
 
222
- // 表情包插件预加载
223
- if (PlugObj.Name === 'emoticons') {
224
- const emoPlug = new PlugObj(this) as EmoticonsPlug
225
- this.plugList[PlugObj.Name] = emoPlug
226
-
227
- window.setTimeout(() => {
228
- emoPlug.loadEmoticonsData()
229
- }, 1000) // 延迟 1s 加载
230
- }
231
-
232
225
  btnElem.addEventListener('click', () => {
233
226
  let plug = this.plugList[PlugObj.Name]
234
227
  if (!plug) {
@@ -272,6 +265,16 @@ export default class Editor extends Component {
272
265
  })
273
266
  })
274
267
 
268
+ // 表情包插件预加载
269
+ if (this.conf.emoticons) {
270
+ const emoPlug = new EmoticonsPlug(this)
271
+ this.plugList[EmoticonsPlug.Name] = emoPlug
272
+
273
+ window.setTimeout(() => {
274
+ emoPlug.loadEmoticonsData()
275
+ }, 1000) // 延迟 1s 加载
276
+ }
277
+
275
278
  this.initImgUpload()
276
279
  }
277
280
 
@@ -308,6 +308,12 @@ export default class ListLite extends Component {
308
308
  // 子评论查找回复对象
309
309
  comment.replyTo = (cData.rid ? ctxData.find(c => c.id === cData.rid) : undefined)
310
310
 
311
+ // 渲染元素
312
+ comment.render()
313
+
314
+ // 放入 comment 总表中
315
+ this.commentList.push(comment)
316
+
311
317
  return comment
312
318
  }
313
319
 
@@ -328,28 +334,21 @@ export default class ListLite extends Component {
328
334
  const rootNodes = ListNest.makeNestCommentNodeList(srcData, this.nestSortBy, this.conf.nestMax)
329
335
  rootNodes.forEach((rootNode: ListNest.CommentNode) => {
330
336
  const rootC = this.createComment(rootNode.comment, srcData)
331
- rootC.render()
332
337
 
333
338
  // 显示并播放渐入动画
334
339
  this.$commentsWrap.appendChild(rootC.getEl())
335
340
  rootC.playFadeInAnim()
336
341
 
337
- // 评论放入 comments 总表中
338
- this.commentList.push(rootC)
339
-
340
342
  // 加载子评论
341
343
  const that = this
342
344
  ;(function loadChildren(parentC: Comment, parentNode: ListNest.CommentNode) {
343
345
  parentNode.children.forEach((node: ListNest.CommentNode) => {
344
346
  const childD = node.comment
345
347
  const childC = that.createComment(childD, srcData)
346
- childC.render()
347
348
 
348
349
  // 插入到父评论中
349
350
  parentC.putChild(childC)
350
351
 
351
- that.commentList.push(childC)
352
-
353
352
  // 递归加载子评论
354
353
  loadChildren(childC, node)
355
354
  })
@@ -364,10 +363,6 @@ export default class ListLite extends Component {
364
363
  private putCommentFlatMode(cData: CommentData, ctxData: CommentData[], insertMode: 'append'|'prepend') {
365
364
  if (cData.is_collapsed) cData.is_allow_reply = false
366
365
  const comment = this.createComment(cData, ctxData)
367
- comment.render()
368
-
369
- // 将评论导入 comments 总表中
370
- this.commentList.push(comment)
371
366
 
372
367
  // 可见评论添加到界面
373
368
  // 注:不可见评论用于显示 “引用内容”
@@ -391,14 +386,11 @@ export default class ListLite extends Component {
391
386
 
392
387
  if (commentData.rid === 0) {
393
388
  // root评论 新增
394
- comment.render()
395
389
  this.$commentsWrap.prepend(comment.getEl())
396
- this.commentList.push(comment)
397
390
  } else {
398
391
  // 子评论 新增
399
392
  const parent = this.findComment(commentData.rid)
400
393
  if (parent) {
401
- comment.render()
402
394
  parent.putChild(comment, (this.nestSortBy === 'DATE_ASC' ? 'append' : 'prepend'))
403
395
 
404
396
  // 若父评论存在 “子评论部分” 限高,取消限高
package/src/defaults.ts CHANGED
@@ -9,7 +9,7 @@ const defaults: ArtalkConfig = {
9
9
  placeholder: '键入内容...',
10
10
  noComment: '「此时无声胜有声」',
11
11
  sendBtn: '发送评论',
12
- darkMode: false,
12
+ darkMode: 'auto',
13
13
  editorTravel: true,
14
14
 
15
15
  flatMode: 'auto',
@@ -27,7 +27,7 @@ export default interface ArtalkConfig {
27
27
  editorTravel?: boolean
28
28
 
29
29
  /** 表情包 */
30
- emoticons?: any
30
+ emoticons?: object|any[]|string|false
31
31
 
32
32
  /** Gravatar 头像 */
33
33
  gravatar?: {
@@ -66,7 +66,7 @@ export default interface ArtalkConfig {
66
66
  pvEl?: string
67
67
 
68
68
  /** 暗黑模式 */
69
- darkMode?: boolean
69
+ darkMode?: boolean|'auto'
70
70
 
71
71
  /** 请求超时(单位:秒) */
72
72
  reqTimeout?: number
package/vite.config.ts CHANGED
@@ -15,18 +15,12 @@ export default defineConfig({
15
15
  formats: ["es", "umd", "iife"]
16
16
  },
17
17
  rollupOptions: {
18
- // 确保外部化处理那些你不想打包进库的依赖
19
- external: ['vue'],
20
18
  output: {
21
19
  assetFileNames: assetInfo => {
22
20
  if (/\.css$/.test(assetInfo.name)) {
23
21
  return 'Artalk.css'
24
22
  }
25
23
  return "[name].[ext]"
26
- },
27
- // 在 UMD 构建模式下为这些外部化的依赖提供一个全局变量
28
- globals: {
29
- vue: 'Vue'
30
24
  }
31
25
  }
32
26
  }