artalk 2.2.12 → 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 +3216 -2883
- 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 +2 -2
- package/package.json +6 -5
- package/src/api/index.ts +20 -1
- package/src/api/request.ts +7 -7
- package/src/artalk.ts +68 -50
- 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 +16 -5
- 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 +80 -99
- 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 +50 -32
- 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 -12
- 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 -607
- 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,17 +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
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 '
|
|
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 = {
|
package/src/api/request.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
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> {
|
|
@@ -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(
|
|
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.
|
|
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.
|
|
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('
|
|
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,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
|
|
|
@@ -220,19 +214,43 @@ export default class Artalk {
|
|
|
220
214
|
}
|
|
221
215
|
}
|
|
222
216
|
|
|
223
|
-
/** PV */
|
|
224
|
-
public async
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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
|
+
}
|
|
228
224
|
|
|
229
|
-
|
|
230
|
-
|
|
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
|
+
}
|
|
231
232
|
|
|
232
|
-
|
|
233
|
-
|
|
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
|
+
}
|
|
234
249
|
|
|
235
|
-
|
|
250
|
+
document.querySelectorAll(args.countEl).forEach((el) => {
|
|
251
|
+
const pageKey = el.getAttribute('data-page-key') || curtPageKey
|
|
252
|
+
el.innerHTML = `${Number(pageCounts[pageKey] || 0)}`
|
|
253
|
+
})
|
|
236
254
|
}
|
|
237
255
|
|
|
238
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
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { CommentData } from '~/types/artalk-data'
|
|
2
|
+
import Context from '~/types/context'
|
|
3
|
+
import Component from '../lib/component'
|
|
4
|
+
import * as Utils from '../lib/utils'
|
|
5
|
+
import UADetect from '../lib/detect'
|
|
6
|
+
import CommentRender from './render'
|
|
7
|
+
import CommentActions from './actions'
|
|
8
|
+
|
|
9
|
+
export interface CommentConf {
|
|
10
|
+
isUnread?: boolean
|
|
11
|
+
openURL?: string
|
|
12
|
+
isFlatMode: boolean
|
|
13
|
+
replyTo?: CommentData
|
|
14
|
+
afterRender?: () => void
|
|
15
|
+
openEvt?: () => void
|
|
16
|
+
onReplyBtnClick?: Function
|
|
17
|
+
onDelete?: Function
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export default class Comment extends Component {
|
|
21
|
+
private renderInstance: CommentRender
|
|
22
|
+
private actionInstance: CommentActions
|
|
23
|
+
|
|
24
|
+
private data: CommentData
|
|
25
|
+
private cConf: CommentConf
|
|
26
|
+
|
|
27
|
+
private parent: Comment|null
|
|
28
|
+
private children: Comment[] = []
|
|
29
|
+
|
|
30
|
+
private nestCurt: number // 当前嵌套层数
|
|
31
|
+
private nestMax: number // 最大嵌套层数
|
|
32
|
+
|
|
33
|
+
constructor(ctx: Context, data: CommentData, conf: CommentConf) {
|
|
34
|
+
super(ctx)
|
|
35
|
+
|
|
36
|
+
// 最大嵌套数
|
|
37
|
+
this.nestMax = ctx.conf.nestMax || 3
|
|
38
|
+
|
|
39
|
+
this.cConf = conf
|
|
40
|
+
this.data = { ...data }
|
|
41
|
+
this.data.date = this.data.date.replace(/-/g, '/') // 解决 Safari 日期解析 NaN 问题
|
|
42
|
+
|
|
43
|
+
this.parent = null
|
|
44
|
+
this.nestCurt = 1 // 现在已嵌套 n 层
|
|
45
|
+
|
|
46
|
+
this.actionInstance = new CommentActions(this)
|
|
47
|
+
this.renderInstance = new CommentRender(this)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** 渲染 UI */
|
|
51
|
+
public render() {
|
|
52
|
+
const newEl = this.renderInstance.render()
|
|
53
|
+
|
|
54
|
+
if (this.$el) this.$el.replaceWith(newEl)
|
|
55
|
+
this.$el = newEl
|
|
56
|
+
|
|
57
|
+
if (this.cConf.afterRender) this.cConf.afterRender()
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** 获取评论操作实例对象 */
|
|
61
|
+
public getActions() {
|
|
62
|
+
return this.actionInstance
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** 获取评论渲染器实例对象 */
|
|
66
|
+
public getRender() {
|
|
67
|
+
return this.renderInstance
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** 获取评论数据 */
|
|
71
|
+
public getData() {
|
|
72
|
+
return this.data
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** 设置数据 */
|
|
76
|
+
public setData(data: CommentData) {
|
|
77
|
+
this.data = data
|
|
78
|
+
|
|
79
|
+
this.render()
|
|
80
|
+
this.getRender().playFadeAnimForBody()
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** 获取父评论 */
|
|
84
|
+
public getParent() {
|
|
85
|
+
return this.parent
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** 获取所有子评论 */
|
|
89
|
+
public getChildren() {
|
|
90
|
+
return this.children
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** 获取当前嵌套层数 */
|
|
94
|
+
public getNestCurt() {
|
|
95
|
+
return this.nestCurt
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** 判断是否为根评论 */
|
|
99
|
+
public getIsRoot() {
|
|
100
|
+
return this.data.rid === 0
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** 获取评论 ID */
|
|
104
|
+
public getID() {
|
|
105
|
+
return this.data.id
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** 置入子评论 */
|
|
109
|
+
public putChild(childC: Comment, insertMode: 'append'|'prepend' = 'append') {
|
|
110
|
+
childC.parent = this
|
|
111
|
+
childC.nestCurt = this.nestCurt + 1 // 嵌套层数 +1
|
|
112
|
+
|
|
113
|
+
this.children.push(childC)
|
|
114
|
+
|
|
115
|
+
const $children = this.getChildrenEl()
|
|
116
|
+
if (insertMode === 'append') $children.append(childC.getEl())
|
|
117
|
+
else if (insertMode === 'prepend') $children.prepend(childC.getEl())
|
|
118
|
+
|
|
119
|
+
childC.getRender().playFadeAnim()
|
|
120
|
+
|
|
121
|
+
// 内容限高
|
|
122
|
+
childC.getRender().checkHeightLimitArea('content')
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** 获取存放子评论的元素对象 */
|
|
126
|
+
public getChildrenEl(): HTMLElement {
|
|
127
|
+
let $children = this.getRender().getChildrenWrap()
|
|
128
|
+
|
|
129
|
+
if (!$children) {
|
|
130
|
+
// console.log(this.nestCurt)
|
|
131
|
+
if (this.nestCurt < this.nestMax) {
|
|
132
|
+
$children = this.getRender().renderChildrenWrap()
|
|
133
|
+
} else {
|
|
134
|
+
$children = this.parent!.getChildrenEl()
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return $children
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** 获取所有父评论 */
|
|
142
|
+
public getParents() {
|
|
143
|
+
const parents: Comment[] = []
|
|
144
|
+
const once = (c: Comment) => {
|
|
145
|
+
if (c.parent) {
|
|
146
|
+
parents.push(c.parent)
|
|
147
|
+
once(c.parent)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
once(this)
|
|
152
|
+
return parents
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** 获取评论元素对象 */
|
|
156
|
+
public getEl() {
|
|
157
|
+
return this.$el
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** 获取 Gravatar 头像 URL */
|
|
161
|
+
public getGravatarURL() {
|
|
162
|
+
return Utils.getGravatarURL(this.ctx, this.data.email_encrypted)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** 获取评论 markdown 解析后的内容 */
|
|
166
|
+
public getContentMarked() {
|
|
167
|
+
return Utils.marked(this.ctx, this.data.content)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** 获取格式化后的日期 */
|
|
171
|
+
public getDateFormatted() {
|
|
172
|
+
return Utils.timeAgo(new Date(this.data.date), this.ctx)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** 获取用户 UserAgent 浏览器 */
|
|
176
|
+
public getUserUaBrowser() {
|
|
177
|
+
const info = UADetect(this.data.ua)
|
|
178
|
+
return `${info.browser} ${info.version}`
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** 获取用户 UserAgent 系统 */
|
|
182
|
+
public getUserUaOS() {
|
|
183
|
+
const info = UADetect(this.data.ua)
|
|
184
|
+
return `${info.os} ${info.osVersion}`
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** 获取配置 */
|
|
188
|
+
public getConf() {
|
|
189
|
+
return this.cConf
|
|
190
|
+
}
|
|
191
|
+
}
|