artalk 2.2.12 → 2.3.2

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 (78) hide show
  1. package/dist/Artalk.css +1 -1
  2. package/dist/Artalk.es.js +1535 -1175
  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 +5123 -0
  7. package/dist/ArtalkLite.iife.js +1 -0
  8. package/dist/ArtalkLite.js +1 -0
  9. package/index.html +2 -2
  10. package/package.json +6 -5
  11. package/src/api/index.ts +20 -1
  12. package/src/api/request.ts +7 -7
  13. package/src/artalk.ts +117 -121
  14. package/src/comment/actions.ts +86 -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 +204 -13
  23. package/src/defaults.ts +16 -5
  24. package/src/editor/editor.html +18 -0
  25. package/src/editor/editor.ts +419 -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 +2 -1
  29. package/src/{components/editor-plugs → editor/plugs}/emoticons-plug.ts +65 -63
  30. package/src/editor/plugs/header-input-plug.ts +78 -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 +124 -0
  34. package/src/i18n/en.ts +78 -0
  35. package/src/i18n/index.ts +21 -0
  36. package/src/i18n/zh-CN.ts +82 -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 -47
  41. package/src/lib/checker/admin-checker.ts +7 -7
  42. package/src/lib/checker/captcha-checker.ts +5 -6
  43. package/src/lib/checker/index.ts +22 -17
  44. package/src/lib/component.ts +7 -2
  45. package/src/lib/stat.ts +63 -0
  46. package/src/lib/ui.ts +6 -12
  47. package/src/lib/user.ts +20 -3
  48. package/src/lib/utils.ts +23 -9
  49. package/src/list/index.ts +3 -0
  50. package/src/{components → list}/list-lite.ts +80 -100
  51. package/src/{components → list}/list-nest.ts +0 -0
  52. package/src/{components/html → list}/list.html +3 -6
  53. package/src/{components → list}/list.ts +44 -42
  54. package/src/style/_variables.less +1 -2
  55. package/src/style/comment.less +26 -13
  56. package/src/style/editor.less +22 -14
  57. package/src/style/list.less +6 -9
  58. package/src/style/main.less +9 -8
  59. package/src/style/pagination.less +1 -1
  60. package/test/ArtalkCount.html +16 -0
  61. package/test/ArtalkI18n.html +15 -0
  62. package/test/ArtalkLite.html +15 -0
  63. package/types/artalk-config.d.ts +50 -32
  64. package/types/context.d.ts +128 -0
  65. package/types/event.d.ts +6 -66
  66. package/types/global.d.ts +1 -0
  67. package/types/plug.d.ts +4 -0
  68. package/{vite-common.config.ts → vite-base.config.ts} +6 -4
  69. package/vite-demo.config.ts +2 -4
  70. package/vite-lite.config.ts +22 -0
  71. package/vite.config.ts +9 -12
  72. package/src/components/_component.ts.txt +0 -10
  73. package/src/components/comment.ts +0 -605
  74. package/src/components/editor-plugs/_example-plug.ts.txt +0 -29
  75. package/src/components/editor-plugs/editor-plug.ts +0 -20
  76. package/src/components/editor-plugs/preview-plug.ts +0 -48
  77. package/src/components/editor.ts +0 -607
  78. 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.12",
4
- "backendMinVersion": "2.1.6",
3
+ "version": "2.3.2",
4
+ "backendMinVersion": "2.1.7",
5
5
  "license": "LGPL-3.0",
6
6
  "description": "A Selfhosted Comment System",
7
7
  "keywords": [
@@ -24,17 +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
33
  "abortcontroller-polyfill": "^1.7.3"
34
34
  },
35
35
  "scripts": {
36
36
  "dev": "vite",
37
- "build": "pnpm lint && vite build",
37
+ "build": "pnpm lint && vite build && pnpm build:lite",
38
+ "build:lite": "vite build --config vite-lite.config.ts",
38
39
  "build:demo": "vite build --config vite-demo.config.ts",
39
40
  "serve": "vite preview",
40
41
  "deploy": "gh-pages -d deploy",
package/src/api/index.ts CHANGED
@@ -1,6 +1,6 @@
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
6
  import 'abortcontroller-polyfill/dist/polyfill-patch-fetch'
@@ -274,6 +274,25 @@ export default class Api {
274
274
  return p.pv as number
275
275
  }
276
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
+
277
296
  /** 图片上传 */
278
297
  public async imgUpload(file: File) {
279
298
  const params: any = {
@@ -1,4 +1,4 @@
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> {
@@ -10,14 +10,14 @@ export async function Fetch(ctx: Context, input: RequestInfo, init: RequestInit,
10
10
  }
11
11
 
12
12
  // 请求操作
13
- const resp = await timeoutFetch(input, timeout || ctx.conf.reqTimeout || 15000, init)
13
+ const resp = await timeoutFetch(ctx, input, timeout || ctx.conf.reqTimeout || 15000, init)
14
14
 
15
15
  const respHttpCode = resp.status
16
16
  const noAccessCodes = [401, 400]
17
17
  const isNoAccess = noAccessCodes.includes(respHttpCode)
18
18
 
19
19
  if (!resp.ok && !isNoAccess)
20
- throw new Error(`请求响应 ${respHttpCode}`)
20
+ throw new Error(`${ctx.$t('reqGot')} ${respHttpCode}`)
21
21
 
22
22
  // 解析获取响应的 json
23
23
  let json: any = await resp.json()
@@ -33,7 +33,7 @@ export async function Fetch(ctx: Context, input: RequestInfo, init: RequestInit,
33
33
  if (json.data?.need_captcha) {
34
34
  // 请求需要验证码
35
35
  json = await (new Promise<any>((resolve, reject) => {
36
- ctx.trigger('checker-captcha', {
36
+ ctx.checkCaptcha({
37
37
  imgData: json.data.img_data,
38
38
  iframe: json.data.iframe,
39
39
  onSuccess: () => { recall(resolve, reject) },
@@ -43,7 +43,7 @@ export async function Fetch(ctx: Context, input: RequestInfo, init: RequestInit,
43
43
  } else if (json.data?.need_login || isNoAccess) {
44
44
  // 请求需要管理员权限
45
45
  json = await (new Promise<any>((resolve, reject) => {
46
- ctx.trigger('checker-admin', {
46
+ ctx.checkAdmin({
47
47
  onSuccess: () => { recall(resolve, reject) },
48
48
  onCancel: () => { reject(json) }
49
49
  })
@@ -82,7 +82,7 @@ export function ToFormData(object: {[key: string]: any}): FormData {
82
82
  }
83
83
 
84
84
  /** 我靠,fetch 一个 timeout,都要丑陋的实现 */
85
- function timeoutFetch(url: RequestInfo, ms: number, opts: RequestInit) {
85
+ function timeoutFetch(ctx: Context, url: RequestInfo, ms: number, opts: RequestInit) {
86
86
  const controller = new AbortController()
87
87
  opts.signal?.addEventListener('abort', () => controller.abort()) // 保留原有 signal 功能
88
88
  let promise = fetch(url, { ...opts, signal: controller.signal })
@@ -91,7 +91,7 @@ function timeoutFetch(url: RequestInfo, ms: number, opts: RequestInit) {
91
91
  promise.finally(() => { clearTimeout(timer) })
92
92
  }
93
93
  promise = promise.catch((err) => {
94
- throw ((err || {}).name === 'AbortError') ? new Error('请求超时或意外终止') : err
94
+ throw ((err || {}).name === 'AbortError') ? new Error(ctx.$t('reqAborted')) : err
95
95
  })
96
96
  return promise
97
97
  }
package/src/artalk.ts CHANGED
@@ -2,28 +2,32 @@ 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 ArtalkPlug from '~/types/plug'
6
+ import Context from '~/types/context'
7
+ import ConcreteContext from './context'
6
8
  import defaults from './defaults'
9
+ import { internal as internalLocales } from './i18n'
7
10
 
8
11
  import CheckerLauncher from './lib/checker'
9
- import Editor from './components/editor'
10
- import List from './components/list'
11
- import SidebarLayer from './components/sidebar-layer'
12
+ import Editor from './editor'
13
+ import List from './list'
14
+ import SidebarLayer from './layer/sidebar-layer'
12
15
 
13
- import Layer, { GetLayerWrap } from './components/layer'
14
- import Api from './api'
16
+ import Layer, { GetLayerWrap } from './layer'
15
17
  import * as Utils from './lib/utils'
16
18
  import * as Ui from './lib/ui'
19
+ import * as Stat from './lib/stat'
17
20
 
18
21
  /**
19
22
  * Artalk
20
- * @link https://artalk.js.org
23
+ *
24
+ * @website https://artalk.js.org
21
25
  */
22
26
  export default class Artalk {
23
27
  public static readonly defaults: ArtalkConfig = defaults
24
28
 
25
- public ctx!: Context
26
29
  public conf!: ArtalkConfig
30
+ public ctx!: Context
27
31
  public $root!: HTMLElement
28
32
 
29
33
  public checkerLauncher!: CheckerLauncher
@@ -31,79 +35,49 @@ export default class Artalk {
31
35
  public list!: List
32
36
  public sidebarLayer!: SidebarLayer
33
37
 
34
- constructor (customConf: ArtalkConfig) {
35
- // 配置
36
- this.conf = Utils.mergeDeep(Artalk.defaults, customConf)
37
- this.conf.server = this.conf.server.replace(/\/$/, '').replace(/\/api\/?$/, '')
38
+ protected static Plugins: ArtalkPlug[] = [
39
+ Stat.PvCountWidget
40
+ ]
38
41
 
39
- // 默认 pageKey
40
- if (!this.conf.pageKey) {
41
- // @link http://bl.ocks.org/abernier/3070589
42
- this.conf.pageKey = `${window.location.pathname}`
43
- }
42
+ constructor(customConf: Partial<ArtalkConfig>) {
43
+ /* 初始化基本配置 */
44
+ this.conf = Artalk.HandelBaseConf(customConf)
45
+ if (this.conf.el instanceof HTMLElement) this.$root = this.conf.el
44
46
 
45
- // 默认 pageTitle
46
- if (!this.conf.pageTitle) {
47
- this.conf.pageTitle = `${document.title}`
48
- }
47
+ /* 初始化 Context */
48
+ this.ctx = new ConcreteContext(this.conf, this.$root)
49
49
 
50
- // 列表显示模式
51
- if (this.conf.nestMax && this.conf.nestMax <= 1) {
52
- this.conf.flatMode = true
53
- }
50
+ // @ts-ignore 远程加载引用后端的配置
51
+ if (this.conf.useBackendConf) return this.loadConfRemoteAndInitComponents()
54
52
 
55
- // 装载元素
56
- if (!!this.conf.el && this.conf.el instanceof HTMLElement) {
57
- this.$root = this.conf.el
58
- } else {
59
- try {
60
- const $root = document.querySelector<HTMLElement>(this.conf.el)
61
- if (!$root) throw Error(`Sorry, target element "${this.conf.el}" was not found.`)
62
- this.$root = $root
63
- } catch (e) {
64
- console.error(e)
65
- throw new Error('Please check your Artalk `el` config.')
66
- }
67
- }
68
-
69
- // Context 初始化
70
- this.ctx = new Context(this.$root, this.conf)
71
-
72
- // 界面初始化
73
- this.$root.classList.add('artalk')
74
- this.$root.innerHTML = ''
75
-
76
- // 初始化组件
77
- if (this.conf.useBackendConf) {
78
- // 复用后端的配置
79
- // @ts-ignore
80
- return (async () => {
81
- await this.loadConfRemote()
82
- this.initCore()
83
- return this // Promise<Artalk>
84
- })()
85
- }
86
-
87
- this.initCore()
53
+ /* 初始化组件 */
54
+ this.initComponents()
88
55
  }
89
56
 
90
- private initCore() {
91
- // 组件初始化
57
+ /** 组件初始化 */
58
+ private initComponents() {
59
+ this.initLocale()
92
60
  this.initLayer()
93
61
  this.initDarkMode()
62
+ Utils.initMarked(this.ctx)
63
+
64
+ // CheckerLauncher
94
65
  this.checkerLauncher = new CheckerLauncher(this.ctx)
95
- Utils.initMarked(this.ctx) // 初始化 marked
66
+ this.ctx.setCheckerLauncher(this.checkerLauncher)
96
67
 
97
68
  // 编辑器
98
69
  this.editor = new Editor(this.ctx)
70
+ this.ctx.setEditor(this.editor)
99
71
  this.$root.appendChild(this.editor.$el)
100
72
 
101
73
  // 评论列表
102
74
  this.list = new List(this.ctx)
75
+ this.ctx.setList(this.list)
103
76
  this.$root.appendChild(this.list.$el)
104
77
 
105
78
  // 侧边栏
106
79
  this.sidebarLayer = new SidebarLayer(this.ctx)
80
+ this.ctx.setSidebarLayer(this.sidebarLayer)
107
81
  this.$root.appendChild(this.sidebarLayer.$el)
108
82
 
109
83
  // 评论获取
@@ -112,29 +86,66 @@ export default class Artalk {
112
86
  // 事件绑定初始化
113
87
  this.initEventBind()
114
88
 
115
- // 其他
116
- this.initPV()
117
-
118
89
  // 插件初始化
119
90
  Artalk.Plugins.forEach(plugin => {
120
- if (typeof plugin === 'function') {
91
+ if (typeof plugin === 'function')
121
92
  plugin(this.ctx)
122
- }
123
93
  })
124
94
  }
125
95
 
126
- /** 获取远程配置 */
96
+ /** 基本配置初始化 */
97
+ private static HandelBaseConf(customConf: Partial<ArtalkConfig>): ArtalkConfig {
98
+ // 合并默认配置
99
+ const conf: ArtalkConfig = Utils.mergeDeep(Artalk.defaults, customConf)
100
+
101
+ // 绑定元素
102
+ if (typeof conf.el === 'string' && !!conf.el) {
103
+ try {
104
+ const findEl = document.querySelector<HTMLElement>(conf.el)
105
+ if (!findEl) throw Error(`Target element "${conf.el}" was not found.`)
106
+ conf.el = findEl
107
+ } catch (e) {
108
+ console.error(e)
109
+ throw new Error('Please check your Artalk `el` config.')
110
+ }
111
+ }
112
+
113
+ // 服务器配置
114
+ conf.server = conf.server.replace(/\/$/, '').replace(/\/api\/?$/, '')
115
+
116
+ // 默认 pageKey
117
+ if (!conf.pageKey) {
118
+ // @link http://bl.ocks.org/abernier/3070589
119
+ conf.pageKey = `${window.location.pathname}`
120
+ }
121
+
122
+ // 默认 pageTitle
123
+ if (!conf.pageTitle) {
124
+ conf.pageTitle = `${document.title}`
125
+ }
126
+
127
+ return conf
128
+ }
129
+
130
+ /** 初始化组件根据远程加载的配置 */
131
+ private async loadConfRemoteAndInitComponents() {
132
+ await this.loadConfRemote()
133
+ this.initComponents()
134
+ return this
135
+ }
136
+
137
+ /** 远程加载配置 */
127
138
  private async loadConfRemote() {
128
139
  Ui.showLoading(this.$root)
129
140
  let backendConf = {}
130
141
  try {
131
- backendConf = await (new Api(this.ctx)).conf()
132
- } catch (err) { console.error("配置远程获取失败", err) }
142
+ backendConf = await this.ctx.getApi().conf()
143
+ } catch (err) { console.error("Load config from remote err", err) }
133
144
  this.ctx.conf = Utils.mergeDeep(this.ctx.conf, backendConf)
134
145
  Ui.hideLoading(this.$root)
135
146
  }
136
147
 
137
- /** 事件绑定 · 初始化 */
148
+ /** 事件绑定初始化 */
138
149
  private initEventBind() {
139
150
  // 锚点快速跳转评论
140
151
  window.addEventListener('hashchange', () => {
@@ -142,50 +153,41 @@ export default class Artalk {
142
153
  this.list.checkGoToCommentByUrlHash()
143
154
  })
144
155
 
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
156
  // 本地用户数据变更
169
157
  this.ctx.on('user-changed', () => {
170
- this.ctx.trigger('check-admin-show-el')
171
- this.ctx.trigger('list-refresh-ui')
158
+ this.ctx.checkAdminShowEl()
159
+ this.ctx.listRefreshUI()
172
160
  })
173
161
  }
174
162
 
175
- /** 重新加载 */
176
- public reload() {
177
- this.list.fetchComments(0)
163
+ /** 语言初始化 */
164
+ private initLocale() {
165
+ if (typeof this.conf.locale === 'string') {
166
+ if (this.conf.locale === 'auto') { // 自动切换
167
+ this.conf.locale = ((navigator.languages) ? navigator.languages[0] : navigator.language)
168
+ }
169
+
170
+ this.conf.locale = this.conf.locale.replace(
171
+ /^([a-zA-Z]+)(-[a-zA-Z]+)?$/,
172
+ (_, p1: string, p2: string) => (p1.toLowerCase() + (p2 || '').toUpperCase())
173
+ )
174
+
175
+ if (!internalLocales[this.conf.locale]) { // 语言不存在时,使用 en
176
+ console.log(`Locale "${this.conf.locale}" not found.`)
177
+ this.conf.locale = 'en'
178
+ }
179
+ }
178
180
  }
179
181
 
180
182
  /** Layer 初始化 */
181
- initLayer() {
183
+ private initLayer() {
182
184
  // 记录页面原始 Styles
183
185
  Layer.BodyOrgOverflow = document.body.style.overflow
184
186
  Layer.BodyOrgPaddingRight = document.body.style.paddingRight
185
187
  }
186
188
 
187
- /** 暗黑模式 · 初始化 */
188
- public initDarkMode() {
189
+ /** 暗黑模式初始化 */
190
+ private initDarkMode() {
189
191
  if (this.conf.darkMode === 'auto') {
190
192
  // 自动切换暗黑模式,事件监听
191
193
  const darkModeMedia = window.matchMedia('(prefers-color-scheme: dark)')
@@ -196,7 +198,7 @@ export default class Artalk {
196
198
  }
197
199
  }
198
200
 
199
- /** 暗黑模式 · 设定 */
201
+ /** 设置暗黑模式 */
200
202
  public setDarkMode(darkMode: boolean) {
201
203
  const darkModeClassName = 'atk-dark-mode'
202
204
 
@@ -220,21 +222,6 @@ export default class Artalk {
220
222
  }
221
223
  }
222
224
 
223
- /** PV */
224
- public async initPV() {
225
- const pvNum = await new Api(this.ctx).pv()
226
- if (Number.isNaN(Number(pvNum)))
227
- return
228
-
229
- if (!this.conf.pvEl || !document.querySelector(this.conf.pvEl))
230
- return
231
-
232
- const $pv = document.querySelector<HTMLElement>(this.conf.pvEl)!
233
- // $pv.innerText = '-' // 默认占位符
234
-
235
- $pv.innerText = String(pvNum)
236
- }
237
-
238
225
  /** 监听事件 */
239
226
  public on<K extends keyof EventPayloadMap>(name: K, handler: Handler<EventPayloadMap[K]>) {
240
227
  this.ctx.on(name, handler, 'external')
@@ -250,11 +237,20 @@ export default class Artalk {
250
237
  this.ctx.trigger(name, payload, 'external')
251
238
  }
252
239
 
253
- /** Plugins */
254
- protected static Plugins: ((ctx: Context) => void)[] = []
240
+ /** 重新加载 */
241
+ public reload() {
242
+ this.ctx.listReload()
243
+ }
255
244
 
256
- /** Enable Plugin */
257
- public static Use(plugin: ((ctx: Context) => void)) {
245
+ /** Use Plugin */
246
+ public static Use(plugin: ArtalkPlug) {
258
247
  this.Plugins.push(plugin)
259
248
  }
249
+
250
+ /** 装载数量统计元素 */
251
+ public static LoadCountWidget(customConf: Partial<ArtalkConfig>) {
252
+ const conf = this.HandelBaseConf(customConf)
253
+ const ctx = new ConcreteContext(conf)
254
+ Stat.initCountWidget({ ctx, pvAdd: false })
255
+ }
260
256
  }
@@ -0,0 +1,86 @@
1
+ import Comment from './comment'
2
+ import ActionBtn from '../components/action-btn'
3
+
4
+ export default class CommentActions {
5
+ private comment: Comment
6
+
7
+ private get ctx() { return this.comment.ctx }
8
+ private get data() { return this.comment.getData() }
9
+ private get cConf() { return this.comment.getConf() }
10
+
11
+ public constructor(comment: Comment) {
12
+ this.comment = comment
13
+ }
14
+
15
+ /** 投票操作 */
16
+ public vote(type: 'up'|'down') {
17
+ const actionBtn = (type === 'up') ? this.comment.getRender().voteBtnUp : this.comment.getRender().voteBtnDown
18
+
19
+ this.ctx.getApi().vote(this.data.id, `comment_${type}`)
20
+ .then((v) => {
21
+ this.data.vote_up = v.up
22
+ this.data.vote_down = v.down
23
+ this.comment.getRender().voteBtnUp?.updateText()
24
+ this.comment.getRender().voteBtnDown?.updateText()
25
+ })
26
+ .catch((err) => {
27
+ actionBtn?.setError(this.ctx.$t('voteFail'))
28
+ console.log(err)
29
+ })
30
+ }
31
+
32
+ /** 管理员 - 评论状态修改 */
33
+ public adminEdit(type: 'collapsed'|'pending'|'pinned', btnElem: ActionBtn) {
34
+ if (btnElem.isLoading) return // 若正在修改中
35
+
36
+ btnElem.setLoading(true, `${this.ctx.$t('editing')}...`)
37
+
38
+ // 克隆并修改当前数据
39
+ const modify = { ...this.data }
40
+ if (type === 'collapsed') {
41
+ modify.is_collapsed = !modify.is_collapsed
42
+ } else if (type === 'pending') {
43
+ modify.is_pending = !modify.is_pending
44
+ } else if (type === 'pinned') {
45
+ modify.is_pinned = !modify.is_pinned
46
+ }
47
+
48
+ this.ctx.getApi().commentEdit(modify).then((data) => {
49
+ btnElem.setLoading(false)
50
+
51
+ // 刷新当前 Comment UI
52
+ this.comment.setData(data)
53
+
54
+ // 刷新 List UI
55
+ this.ctx.listRefreshUI()
56
+ }).catch((err) => {
57
+ console.error(err)
58
+ btnElem.setError(this.ctx.$t('editFail'))
59
+ })
60
+ }
61
+
62
+ /** 管理员 - 评论删除 */
63
+ public adminDelete(btnElem: ActionBtn) {
64
+ if (btnElem.isLoading) return // 若正在删除中
65
+
66
+ btnElem.setLoading(true, `${this.ctx.$t('deleting')}...`)
67
+ this.ctx.getApi().commentDel(this.data.id, this.data.site_name)
68
+ .then(() => {
69
+ btnElem.setLoading(false)
70
+ if (this.cConf.onDelete) this.cConf.onDelete(this.comment)
71
+ })
72
+ .catch((e) => {
73
+ console.error(e)
74
+ btnElem.setError(this.ctx.$t('deleteFail'))
75
+ })
76
+ }
77
+
78
+ /** 快速跳转到该评论 */
79
+ public goToReplyComment() {
80
+ const origHash = window.location.hash
81
+ const modifyHash = `#atk-comment-${this.data.rid}`
82
+
83
+ window.location.hash = modifyHash
84
+ if (modifyHash === origHash) window.dispatchEvent(new Event('hashchange')) // 强制触发事件
85
+ }
86
+ }
@@ -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">