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.
- package/dist/Artalk.css +1 -1
- package/dist/Artalk.es.js +3046 -2342
- package/dist/Artalk.iife.js +1 -1
- package/dist/Artalk.js +1 -1
- package/dist/ArtalkLite.css +1 -0
- package/dist/ArtalkLite.es.js +5096 -0
- package/dist/ArtalkLite.iife.js +1 -0
- package/dist/ArtalkLite.js +1 -0
- package/index.html +5 -3
- package/package.json +7 -5
- package/src/api/index.ts +24 -4
- package/src/api/request.ts +50 -81
- package/src/artalk.ts +83 -57
- package/src/comment/actions.ts +87 -0
- package/src/{components/html → comment}/comment.html +1 -1
- package/src/comment/comment.ts +191 -0
- package/src/comment/index.ts +3 -0
- package/src/comment/render.ts +425 -0
- package/src/components/action-btn.ts +6 -3
- package/src/components/dialog.ts +7 -3
- package/src/components/read-more-btn.ts +6 -2
- package/src/context.ts +200 -13
- package/src/defaults.ts +19 -6
- package/src/editor/editor.html +18 -0
- package/src/editor/editor.ts +420 -0
- package/src/editor/index.ts +3 -0
- package/src/editor/plugs/editor-plug.ts +51 -0
- package/src/{components/editor-plugs → editor/plugs}/emoticons-plug.less +1 -0
- package/src/{components/editor-plugs → editor/plugs}/emoticons-plug.ts +65 -63
- package/src/editor/plugs/header-input-plug.ts +79 -0
- package/src/{components/editor-plugs → editor/plugs}/preview-plug.less +0 -0
- package/src/editor/plugs/preview-plug.ts +40 -0
- package/src/editor/plugs/upload-plug.ts +125 -0
- package/src/i18n/en.ts +77 -0
- package/src/i18n/index.ts +21 -0
- package/src/i18n/zh-CN.ts +81 -0
- package/src/layer/index.ts +4 -0
- package/src/{components → layer}/layer.ts +9 -9
- package/src/{components/html → layer}/sidebar-layer.html +0 -0
- package/src/{components → layer}/sidebar-layer.ts +17 -46
- package/src/lib/checker/admin-checker.ts +7 -6
- package/src/lib/checker/captcha-checker.ts +5 -5
- package/src/lib/checker/index.ts +22 -17
- package/src/lib/component.ts +7 -2
- package/src/lib/ui.ts +3 -10
- package/src/lib/user.ts +20 -3
- package/src/lib/utils.ts +23 -9
- package/src/list/index.ts +3 -0
- package/src/{components → list}/list-lite.ts +87 -111
- package/src/{components → list}/list-nest.ts +0 -0
- package/src/{components/html → list}/list.html +3 -6
- package/src/{components → list}/list.ts +37 -35
- package/src/style/comment.less +23 -10
- package/src/style/editor.less +16 -10
- package/src/style/list.less +4 -7
- package/src/style/main.less +3 -1
- package/src/style/pagination.less +1 -1
- package/test/ArtalkLite.html +15 -0
- package/types/artalk-config.d.ts +52 -31
- package/types/context.d.ts +128 -0
- package/types/event.d.ts +6 -66
- package/types/global.d.ts +1 -0
- package/{vite-common.config.ts → vite-base.config.ts} +6 -4
- package/vite-demo.config.ts +2 -4
- package/vite-lite.config.ts +22 -0
- package/vite.config.ts +9 -18
- package/src/components/_component.ts.txt +0 -10
- package/src/components/comment.ts +0 -605
- package/src/components/editor-plugs/_example-plug.ts.txt +0 -29
- package/src/components/editor-plugs/editor-plug.ts +0 -20
- package/src/components/editor-plugs/preview-plug.ts +0 -48
- package/src/components/editor.ts +0 -604
- 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.
|
|
4
|
-
"backendMinVersion": "2.1.
|
|
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": "
|
|
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.
|
|
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 '
|
|
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
|
|
package/src/api/request.ts
CHANGED
|
@@ -1,84 +1,58 @@
|
|
|
1
|
-
import Context from '
|
|
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
|
|
8
|
-
|
|
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
|
-
|
|
13
|
+
const resp = await timeoutFetch(ctx, input, timeout || ctx.conf.reqTimeout || 15000, init)
|
|
15
14
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
19
|
+
if (!resp.ok && !isNoAccess)
|
|
20
|
+
throw new Error(`${ctx.$t('reqGot')} ${respHttpCode}`)
|
|
25
21
|
|
|
26
|
-
|
|
22
|
+
// 解析获取响应的 json
|
|
23
|
+
let json: any = await resp.json()
|
|
27
24
|
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
78
|
-
throw new Error(`网络错误`)
|
|
53
|
+
if (!json.success) throw json // throw 相当于 reject(json)
|
|
79
54
|
|
|
80
|
-
|
|
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
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
promise.
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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 '
|
|
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 './
|
|
10
|
-
import List from './
|
|
11
|
-
import SidebarLayer from './
|
|
10
|
+
import Editor from './editor'
|
|
11
|
+
import List from './list'
|
|
12
|
+
import SidebarLayer from './layer/sidebar-layer'
|
|
12
13
|
|
|
13
|
-
import Layer, { GetLayerWrap } from './
|
|
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
|
-
*
|
|
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
|
|
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
|
-
|
|
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(`
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
|
132
|
-
} catch (err) { console.error("
|
|
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.
|
|
171
|
-
this.ctx.
|
|
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
|
|
210
|
-
|
|
211
|
-
this.
|
|
212
|
-
|
|
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
|
-
|
|
222
|
-
|
|
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
|
-
|
|
225
|
-
|
|
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
|
-
|
|
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
|
+
}
|