artalk 2.2.10 → 2.3.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.
Files changed (73) hide show
  1. package/dist/Artalk.css +1 -1
  2. package/dist/Artalk.es.js +3046 -2342
  3. package/dist/Artalk.iife.js +1 -1
  4. package/dist/Artalk.js +1 -1
  5. package/dist/ArtalkLite.css +1 -0
  6. package/dist/ArtalkLite.es.js +5096 -0
  7. package/dist/ArtalkLite.iife.js +1 -0
  8. package/dist/ArtalkLite.js +1 -0
  9. package/index.html +5 -3
  10. package/package.json +7 -5
  11. package/src/api/index.ts +24 -4
  12. package/src/api/request.ts +50 -81
  13. package/src/artalk.ts +83 -57
  14. package/src/comment/actions.ts +87 -0
  15. package/src/{components/html → comment}/comment.html +1 -1
  16. package/src/comment/comment.ts +191 -0
  17. package/src/comment/index.ts +3 -0
  18. package/src/comment/render.ts +425 -0
  19. package/src/components/action-btn.ts +6 -3
  20. package/src/components/dialog.ts +7 -3
  21. package/src/components/read-more-btn.ts +6 -2
  22. package/src/context.ts +200 -13
  23. package/src/defaults.ts +19 -6
  24. package/src/editor/editor.html +18 -0
  25. package/src/editor/editor.ts +420 -0
  26. package/src/editor/index.ts +3 -0
  27. package/src/editor/plugs/editor-plug.ts +51 -0
  28. package/src/{components/editor-plugs → editor/plugs}/emoticons-plug.less +1 -0
  29. package/src/{components/editor-plugs → editor/plugs}/emoticons-plug.ts +65 -63
  30. package/src/editor/plugs/header-input-plug.ts +79 -0
  31. package/src/{components/editor-plugs → editor/plugs}/preview-plug.less +0 -0
  32. package/src/editor/plugs/preview-plug.ts +40 -0
  33. package/src/editor/plugs/upload-plug.ts +125 -0
  34. package/src/i18n/en.ts +77 -0
  35. package/src/i18n/index.ts +21 -0
  36. package/src/i18n/zh-CN.ts +81 -0
  37. package/src/layer/index.ts +4 -0
  38. package/src/{components → layer}/layer.ts +9 -9
  39. package/src/{components/html → layer}/sidebar-layer.html +0 -0
  40. package/src/{components → layer}/sidebar-layer.ts +17 -46
  41. package/src/lib/checker/admin-checker.ts +7 -6
  42. package/src/lib/checker/captcha-checker.ts +5 -5
  43. package/src/lib/checker/index.ts +22 -17
  44. package/src/lib/component.ts +7 -2
  45. package/src/lib/ui.ts +3 -10
  46. package/src/lib/user.ts +20 -3
  47. package/src/lib/utils.ts +23 -9
  48. package/src/list/index.ts +3 -0
  49. package/src/{components → list}/list-lite.ts +87 -111
  50. package/src/{components → list}/list-nest.ts +0 -0
  51. package/src/{components/html → list}/list.html +3 -6
  52. package/src/{components → list}/list.ts +37 -35
  53. package/src/style/comment.less +23 -10
  54. package/src/style/editor.less +16 -10
  55. package/src/style/list.less +4 -7
  56. package/src/style/main.less +3 -1
  57. package/src/style/pagination.less +1 -1
  58. package/test/ArtalkLite.html +15 -0
  59. package/types/artalk-config.d.ts +52 -31
  60. package/types/context.d.ts +128 -0
  61. package/types/event.d.ts +6 -66
  62. package/types/global.d.ts +1 -0
  63. package/{vite-common.config.ts → vite-base.config.ts} +6 -4
  64. package/vite-demo.config.ts +2 -4
  65. package/vite-lite.config.ts +22 -0
  66. package/vite.config.ts +9 -18
  67. package/src/components/_component.ts.txt +0 -10
  68. package/src/components/comment.ts +0 -605
  69. package/src/components/editor-plugs/_example-plug.ts.txt +0 -29
  70. package/src/components/editor-plugs/editor-plug.ts +0 -20
  71. package/src/components/editor-plugs/preview-plug.ts +0 -48
  72. package/src/components/editor.ts +0 -604
  73. package/src/components/html/editor.html +0 -18
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "artalk",
3
- "version": "2.2.10",
4
- "backendMinVersion": "2.1.4",
3
+ "version": "2.3.0",
4
+ "backendMinVersion": "2.1.7",
5
5
  "license": "LGPL-3.0",
6
6
  "description": "A Selfhosted Comment System",
7
7
  "keywords": [
@@ -24,16 +24,18 @@
24
24
  "module": "./dist/Artalk.es.js",
25
25
  "types": "./types/index.d.ts",
26
26
  "devDependencies": {
27
- "@types/marked": "^4.0.3"
27
+ "@types/marked": "4.0.3"
28
28
  },
29
29
  "dependencies": {
30
30
  "hanabi": "^0.4.0",
31
31
  "insane": "^2.6.2",
32
- "marked": "^4.0.14"
32
+ "marked": "^4.0.16",
33
+ "abortcontroller-polyfill": "^1.7.3"
33
34
  },
34
35
  "scripts": {
35
36
  "dev": "vite",
36
- "build": "pnpm lint && vite build",
37
+ "build": "pnpm lint && vite build && pnpm build:lite",
38
+ "build:lite": "vite build --config vite-lite.config.ts",
37
39
  "build:demo": "vite build --config vite-demo.config.ts",
38
40
  "serve": "vite preview",
39
41
  "deploy": "gh-pages -d deploy",
package/src/api/index.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  import { CommentData, ListData, UserData, PageData, SiteData, NotifyData } from '~/types/artalk-data'
2
2
  import ArtalkConfig from '~/types/artalk-config'
3
- import Context from '../context'
3
+ import Context from '~/types/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,
@@ -275,6 +274,25 @@ export default class Api {
275
274
  return p.pv as number
276
275
  }
277
276
 
277
+ /** 统计 */
278
+ public async stat(
279
+ type: 'latest_comments'|'latest_pages'|'pv_most_pages'|'comment_most_pages'|
280
+ 'page_pv'|'site_pv'|'page_comment'|'site_comment',
281
+ pageKeys?: string|string[],
282
+ limit?: number
283
+ ) {
284
+ const params: any = {
285
+ type,
286
+ site_name: this.ctx.conf.site || '',
287
+ }
288
+
289
+ if (pageKeys) params.page_keys = Array.isArray(pageKeys) ? pageKeys.join(',') : pageKeys
290
+ if (limit) params.limit = limit
291
+
292
+ const data = await POST<PageData[]|CommentData[]|object|number>(this.ctx, `${this.baseURL}/stat`, params)
293
+ return data
294
+ }
295
+
278
296
  /** 图片上传 */
279
297
  public async imgUpload(file: File) {
280
298
  const params: any = {
@@ -307,6 +325,8 @@ export default class Api {
307
325
  conf.emoticons = conf.emoticons.trim()
308
326
  if (conf.emoticons.startsWith("[") || conf.emoticons.startsWith("{")) {
309
327
  conf.emoticons = JSON.parse(conf.emoticons) // pase json
328
+ } else if (conf.emoticons === "false") {
329
+ conf.emoticons = false
310
330
  }
311
331
  }
312
332
 
@@ -1,84 +1,58 @@
1
- import Context from '../context'
1
+ import Context from '~/types/context'
2
2
 
3
3
  /** 公共请求函数 */
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(ctx, 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(`${ctx.$t('reqGot')} ${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.checkCaptcha({
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.checkAdmin({
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(ctx: Context, 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(ctx.$t('reqAborted')) : err
127
95
  })
96
+ return promise
128
97
  }
package/src/artalk.ts CHANGED
@@ -2,22 +2,24 @@ import './style/main.less'
2
2
 
3
3
  import ArtalkConfig from '~/types/artalk-config'
4
4
  import { EventPayloadMap, Handler } from '~/types/event'
5
- import Context from './context'
5
+ import Context from '~/types/context'
6
+ import ConcreteContext from './context'
6
7
  import defaults from './defaults'
7
8
 
8
9
  import CheckerLauncher from './lib/checker'
9
- import Editor from './components/editor'
10
- import List from './components/list'
11
- import SidebarLayer from './components/sidebar-layer'
10
+ import Editor from './editor'
11
+ import List from './list'
12
+ import SidebarLayer from './layer/sidebar-layer'
12
13
 
13
- import Layer, { GetLayerWrap } from './components/layer'
14
+ import Layer, { GetLayerWrap } from './layer'
14
15
  import Api from './api'
15
16
  import * as Utils from './lib/utils'
16
17
  import * as Ui from './lib/ui'
17
18
 
18
19
  /**
19
20
  * Artalk
20
- * @link https://artalk.js.org
21
+ *
22
+ * @website https://artalk.js.org
21
23
  */
22
24
  export default class Artalk {
23
25
  public static readonly defaults: ArtalkConfig = defaults
@@ -26,12 +28,13 @@ export default class Artalk {
26
28
  public conf!: ArtalkConfig
27
29
  public $root!: HTMLElement
28
30
 
31
+ public api!: Api
29
32
  public checkerLauncher!: CheckerLauncher
30
33
  public editor!: Editor
31
34
  public list!: List
32
35
  public sidebarLayer!: SidebarLayer
33
36
 
34
- constructor (customConf: ArtalkConfig) {
37
+ constructor(customConf: Partial<ArtalkConfig>) {
35
38
  // 配置
36
39
  this.conf = Utils.mergeDeep(Artalk.defaults, customConf)
37
40
  this.conf.server = this.conf.server.replace(/\/$/, '').replace(/\/api\/?$/, '')
@@ -53,12 +56,14 @@ export default class Artalk {
53
56
  }
54
57
 
55
58
  // 装载元素
56
- if (!!this.conf.el && this.conf.el instanceof HTMLElement) {
59
+ this.conf.el = this.conf.el || document.createElement('div') // 允许 this.conf.el 为空
60
+
61
+ if (this.conf.el instanceof HTMLElement) {
57
62
  this.$root = this.conf.el
58
63
  } else {
59
64
  try {
60
65
  const $root = document.querySelector<HTMLElement>(this.conf.el)
61
- if (!$root) throw Error(`Sorry, target element "${this.conf.el}" was not found.`)
66
+ if (!$root) throw Error(`Target element "${this.conf.el}" was not found.`)
62
67
  this.$root = $root
63
68
  } catch (e) {
64
69
  console.error(e)
@@ -67,7 +72,11 @@ export default class Artalk {
67
72
  }
68
73
 
69
74
  // Context 初始化
70
- this.ctx = new Context(this.$root, this.conf)
75
+ this.ctx = new ConcreteContext(this.$root, this.conf)
76
+
77
+ // API 初始化
78
+ this.api = new Api(this.ctx)
79
+ this.ctx.setApi(this.api)
71
80
 
72
81
  // 界面初始化
73
82
  this.$root.classList.add('artalk')
@@ -88,22 +97,30 @@ export default class Artalk {
88
97
  }
89
98
 
90
99
  private initCore() {
91
- // 组件初始化
100
+ /* 组件初始化 */
92
101
  this.initLayer()
93
102
  this.initDarkMode()
103
+
104
+ // CheckerLauncher
94
105
  this.checkerLauncher = new CheckerLauncher(this.ctx)
95
- Utils.initMarked(this.ctx) // 初始化 marked
106
+ this.ctx.setCheckerLauncher(this.checkerLauncher)
107
+
108
+ // 初始化 marked
109
+ Utils.initMarked(this.ctx)
96
110
 
97
111
  // 编辑器
98
112
  this.editor = new Editor(this.ctx)
113
+ this.ctx.setEditor(this.editor)
99
114
  this.$root.appendChild(this.editor.$el)
100
115
 
101
116
  // 评论列表
102
117
  this.list = new List(this.ctx)
118
+ this.ctx.setList(this.list)
103
119
  this.$root.appendChild(this.list.$el)
104
120
 
105
121
  // 侧边栏
106
122
  this.sidebarLayer = new SidebarLayer(this.ctx)
123
+ this.ctx.setSidebarLayer(this.sidebarLayer)
107
124
  this.$root.appendChild(this.sidebarLayer.$el)
108
125
 
109
126
  // 评论获取
@@ -113,7 +130,7 @@ export default class Artalk {
113
130
  this.initEventBind()
114
131
 
115
132
  // 其他
116
- this.initPV()
133
+ this.initCountAndPV()
117
134
 
118
135
  // 插件初始化
119
136
  Artalk.Plugins.forEach(plugin => {
@@ -128,8 +145,8 @@ export default class Artalk {
128
145
  Ui.showLoading(this.$root)
129
146
  let backendConf = {}
130
147
  try {
131
- backendConf = await (new Api(this.ctx)).conf()
132
- } catch (err) { console.error("配置远程获取失败", err) }
148
+ backendConf = await this.ctx.getApi().conf()
149
+ } catch (err) { console.error("Load config from remote err", err) }
133
150
  this.ctx.conf = Utils.mergeDeep(this.ctx.conf, backendConf)
134
151
  Ui.hideLoading(this.$root)
135
152
  }
@@ -142,33 +159,10 @@ export default class Artalk {
142
159
  this.list.checkGoToCommentByUrlHash()
143
160
  })
144
161
 
145
- // 仅管理员显示控制
146
- this.ctx.on('check-admin-show-el', () => {
147
- const items: HTMLElement[] = []
148
-
149
- this.$root.querySelectorAll<HTMLElement>(`[atk-only-admin-show]`).forEach(item => items.push(item))
150
-
151
- // for layer
152
- const { $wrap: $layerWrap } = GetLayerWrap(this.ctx)
153
- if ($layerWrap) $layerWrap.querySelectorAll<HTMLElement>(`[atk-only-admin-show]`).forEach(item => items.push(item))
154
-
155
- // for sidebar
156
- // TODO: 这个其实应该写在 packages/artalk-sidebar 里面的
157
- const $sidebarEl = document.querySelector<HTMLElement>('.atk-sidebar')
158
- if ($sidebarEl) $sidebarEl.querySelectorAll<HTMLElement>(`[atk-only-admin-show]`).forEach(item => items.push(item))
159
-
160
- items.forEach(($item: HTMLElement) => {
161
- if (this.ctx.user.data.isAdmin)
162
- $item.classList.remove('atk-hide')
163
- else
164
- $item.classList.add('atk-hide')
165
- })
166
- })
167
-
168
162
  // 本地用户数据变更
169
163
  this.ctx.on('user-changed', () => {
170
- this.ctx.trigger('check-admin-show-el')
171
- this.ctx.trigger('list-refresh-ui')
164
+ this.ctx.checkAdminShowEl()
165
+ this.ctx.listRefreshUI()
172
166
  })
173
167
  }
174
168
 
@@ -186,8 +180,23 @@ export default class Artalk {
186
180
 
187
181
  /** 暗黑模式 · 初始化 */
188
182
  public initDarkMode() {
183
+ if (this.conf.darkMode === 'auto') {
184
+ // 自动切换暗黑模式,事件监听
185
+ const darkModeMedia = window.matchMedia('(prefers-color-scheme: dark)')
186
+ darkModeMedia.addEventListener('change', (e) => { this.setDarkMode(e.matches) })
187
+ this.setDarkMode(darkModeMedia.matches)
188
+ } else {
189
+ this.setDarkMode(this.conf.darkMode || false)
190
+ }
191
+ }
192
+
193
+ /** 暗黑模式 · 设定 */
194
+ public setDarkMode(darkMode: boolean) {
189
195
  const darkModeClassName = 'atk-dark-mode'
190
196
 
197
+ this.ctx.conf.darkMode = darkMode
198
+ this.ctx.trigger('conf-updated')
199
+
191
200
  if (this.conf.darkMode) {
192
201
  this.$root.classList.add(darkModeClassName)
193
202
  } else {
@@ -205,26 +214,43 @@ export default class Artalk {
205
214
  }
206
215
  }
207
216
 
208
- /** 暗黑模式 · 设定 */
209
- public setDarkMode(darkMode: boolean) {
210
- this.ctx.conf.darkMode = darkMode
211
- this.ctx.trigger('conf-updated')
212
- this.initDarkMode()
213
- }
214
-
215
- /** PV */
216
- public async initPV() {
217
- const pvNum = await new Api(this.ctx).pv()
218
- if (Number.isNaN(Number(pvNum)))
219
- return
217
+ /** 初始化评论数和 PV 数量展示元素 */
218
+ public async initCountAndPV() {
219
+ // 评论数
220
+ const countEl = this.conf.countEl
221
+ if (countEl && document.querySelector(countEl)) {
222
+ this.handleStatCount({ api: 'page_comment', countEl })
223
+ }
220
224
 
221
- if (!this.conf.pvEl || !document.querySelector(this.conf.pvEl))
222
- return
225
+ // PV
226
+ const curtPagePvNum = await this.ctx.getApi().pv()
227
+ const pvEl = this.conf.pvEl
228
+ if (pvEl && document.querySelector(pvEl)) {
229
+ this.handleStatCount({ api: 'page_pv', countEl: pvEl, curtPageCount: curtPagePvNum })
230
+ }
231
+ }
223
232
 
224
- const $pv = document.querySelector<HTMLElement>(this.conf.pvEl)!
225
- // $pv.innerText = '-' // 默认占位符
233
+ private async handleStatCount(args: { api: 'page_pv'|'page_comment', countEl: string, curtPageCount?: number }) {
234
+ let pageCounts: {[key: string]: number} = {}
235
+
236
+ // 当前页面的统计数
237
+ const curtPageKey = this.ctx.conf.pageKey
238
+ if (args.curtPageCount) pageCounts[curtPageKey] = args.curtPageCount
239
+
240
+ // 查询其他页面的统计数
241
+ let queryPageKeys = Array.from(document.querySelectorAll(args.countEl))
242
+ .map(e => e.getAttribute('data-page-key') || curtPageKey)
243
+ .filter(pageKey => pageCounts[pageKey] === undefined)
244
+ queryPageKeys = [...new Set(queryPageKeys)] // 去重
245
+ if (queryPageKeys.length > 0) {
246
+ const counts: any = await this.ctx.getApi().stat(args.api, queryPageKeys)
247
+ pageCounts = { ...pageCounts, ...counts }
248
+ }
226
249
 
227
- $pv.innerText = String(pvNum)
250
+ document.querySelectorAll(args.countEl).forEach((el) => {
251
+ const pageKey = el.getAttribute('data-page-key') || curtPageKey
252
+ el.innerHTML = `${Number(pageCounts[pageKey] || 0)}`
253
+ })
228
254
  }
229
255
 
230
256
  /** 监听事件 */
@@ -0,0 +1,87 @@
1
+ import Api from '../api'
2
+ import Comment from './comment'
3
+ import ActionBtn from '../components/action-btn'
4
+
5
+ export default class CommentActions {
6
+ private comment: Comment
7
+
8
+ private get ctx() { return this.comment.ctx }
9
+ private get data() { return this.comment.getData() }
10
+ private get cConf() { return this.comment.getConf() }
11
+
12
+ public constructor(comment: Comment) {
13
+ this.comment = comment
14
+ }
15
+
16
+ /** 投票操作 */
17
+ public vote(type: 'up'|'down') {
18
+ const actionBtn = (type === 'up') ? this.comment.getRender().voteBtnUp : this.comment.getRender().voteBtnDown
19
+
20
+ this.ctx.getApi().vote(this.data.id, `comment_${type}`)
21
+ .then((v) => {
22
+ this.data.vote_up = v.up
23
+ this.data.vote_down = v.down
24
+ this.comment.getRender().voteBtnUp?.updateText()
25
+ this.comment.getRender().voteBtnDown?.updateText()
26
+ })
27
+ .catch((err) => {
28
+ actionBtn?.setError(this.ctx.$t('voteFail'))
29
+ console.log(err)
30
+ })
31
+ }
32
+
33
+ /** 管理员 - 评论状态修改 */
34
+ public adminEdit(type: 'collapsed'|'pending'|'pinned', btnElem: ActionBtn) {
35
+ if (btnElem.isLoading) return // 若正在修改中
36
+
37
+ btnElem.setLoading(true, `${this.ctx.$t('editing')}...`)
38
+
39
+ // 克隆并修改当前数据
40
+ const modify = { ...this.data }
41
+ if (type === 'collapsed') {
42
+ modify.is_collapsed = !modify.is_collapsed
43
+ } else if (type === 'pending') {
44
+ modify.is_pending = !modify.is_pending
45
+ } else if (type === 'pinned') {
46
+ modify.is_pinned = !modify.is_pinned
47
+ }
48
+
49
+ this.ctx.getApi().commentEdit(modify).then((data) => {
50
+ btnElem.setLoading(false)
51
+
52
+ // 刷新当前 Comment UI
53
+ this.comment.setData(data)
54
+
55
+ // 刷新 List UI
56
+ this.ctx.listRefreshUI()
57
+ }).catch((err) => {
58
+ console.error(err)
59
+ btnElem.setError(this.ctx.$t('editFail'))
60
+ })
61
+ }
62
+
63
+ /** 管理员 - 评论删除 */
64
+ public adminDelete(btnElem: ActionBtn) {
65
+ if (btnElem.isLoading) return // 若正在删除中
66
+
67
+ btnElem.setLoading(true, `${this.ctx.$t('deleting')}...`)
68
+ this.ctx.getApi().commentDel(this.data.id, this.data.site_name)
69
+ .then(() => {
70
+ btnElem.setLoading(false)
71
+ if (this.cConf.onDelete) this.cConf.onDelete(this.comment)
72
+ })
73
+ .catch((e) => {
74
+ console.error(e)
75
+ btnElem.setError(this.ctx.$t('deleteFail'))
76
+ })
77
+ }
78
+
79
+ /** 快速跳转到该评论 */
80
+ public goToReplyComment() {
81
+ const origHash = window.location.hash
82
+ const modifyHash = `#atk-comment-${this.data.rid}`
83
+
84
+ window.location.hash = modifyHash
85
+ if (modifyHash === origHash) window.dispatchEvent(new Event('hashchange')) // 强制触发事件
86
+ }
87
+ }
@@ -4,7 +4,7 @@
4
4
  <div class="atk-main">
5
5
  <div class="atk-header">
6
6
  <span class="atk-item atk-nick"></span>
7
- <span class="atk-item atk-badge"></span>
7
+ <span class="atk-badge-wrap"></span>
8
8
  <span class="atk-item atk-date"></span>
9
9
  </div>
10
10
  <div class="atk-body">