artalk 2.4.2 → 2.4.4

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.
@@ -19,7 +19,7 @@ declare const ApiComponents: {
19
19
  };
20
20
  declare class Api {
21
21
  protected ctx: Context;
22
- baseURL: string;
22
+ get baseURL(): string;
23
23
  constructor(ctx: Context);
24
24
  }
25
25
  declare type TC = typeof ApiComponents;
@@ -10,4 +10,11 @@ export default class SystemApi extends ApiBase {
10
10
  getSettings(): Promise<string>;
11
11
  /** 保存配置数据 */
12
12
  saveSettings(yamlStr: string): Promise<boolean>;
13
+ /** 获取 API 版本信息 */
14
+ version(): Promise<{
15
+ app: string;
16
+ version: string;
17
+ commit_hash: string;
18
+ fe_min_version: string;
19
+ }>;
13
20
  }
@@ -1,11 +1,14 @@
1
- import { UserData, NotifyData } from '../../types/artalk-data';
1
+ import { UserData, NotifyData, UserDataForAdmin } from '../../types/artalk-data';
2
2
  import ApiBase from './api-base';
3
3
  /**
4
4
  * 用户 API
5
5
  */
6
6
  export default class UserApi extends ApiBase {
7
7
  /** 用户 · 登录 */
8
- login(name: string, email: string, password: string): Promise<string>;
8
+ login(name: string, email: string, password: string): Promise<{
9
+ token: string;
10
+ user: UserData;
11
+ }>;
9
12
  /** 用户 · 获取 */
10
13
  userGet(name: string, email: string): {
11
14
  req: Promise<{
@@ -25,4 +28,15 @@ export default class UserApi extends ApiBase {
25
28
  logout(): Promise<unknown>;
26
29
  /** 已读标记 */
27
30
  markRead(notifyKey: string, readAll?: boolean): Promise<unknown>;
31
+ /** 用户 · 列表 */
32
+ userList(offset?: number, limit?: number, type?: 'all' | 'admin' | 'in_conf'): Promise<{
33
+ users: UserDataForAdmin[];
34
+ total: number;
35
+ }>;
36
+ /** 用户 · 新增 */
37
+ userAdd(user: Partial<UserDataForAdmin>, password?: string): Promise<UserDataForAdmin>;
38
+ /** 用户 · 修改 */
39
+ userEdit(user: Partial<UserDataForAdmin>, password?: string): Promise<UserDataForAdmin>;
40
+ /** 用户 · 删除 */
41
+ userDel(userID: number): Promise<unknown>;
28
42
  }
@@ -25,18 +25,12 @@ export default class Artalk {
25
25
  private initComponents;
26
26
  /** 基本配置初始化 */
27
27
  static HandelBaseConf(customConf: Partial<ArtalkConfig>): ArtalkConfig;
28
- /** 初始化组件根据远程加载的配置 */
29
- private loadConfRemoteAndInitComponents;
30
- /** 远程加载配置 */
31
- private loadConfRemote;
32
28
  /** 事件绑定初始化 */
33
29
  private initEventBind;
34
30
  /** 语言初始化 */
35
31
  private initLocale;
36
32
  /** Layer 初始化 */
37
33
  private initLayer;
38
- /** 暗黑模式初始化 */
39
- private initDarkMode;
40
34
  /** 监听事件 */
41
35
  on<K extends keyof EventPayloadMap>(name: K, handler: Handler<EventPayloadMap[K]>): void;
42
36
  /** 解除监听事件 */
@@ -69,4 +69,8 @@ export default class Context implements ContextApi {
69
69
  [key: string]: string;
70
70
  }): string;
71
71
  setDarkMode(darkMode: boolean): void;
72
+ private darkModeMedia;
73
+ private darkModeAutoFunc?;
74
+ private refreshDarkModeConf;
75
+ updateConf(conf: Partial<ArtalkConfig>): void;
72
76
  }
@@ -9,14 +9,23 @@ export default class ListLite extends Component {
9
9
  protected $commentsWrap: HTMLElement;
10
10
  protected data?: ListData;
11
11
  protected isLoading: boolean;
12
- noCommentText: string;
12
+ noCommentText?: string;
13
+ /** 嵌套模式下的排序方式 */
14
+ private _nestSortBy?;
15
+ get nestSortBy(): ListNest.SortByType;
16
+ set nestSortBy(val: ListNest.SortByType);
13
17
  /** 平铺模式 */
14
- flatMode: boolean;
15
- /** 嵌套模式 */
16
- nestSortBy: ListNest.SortByType;
17
- /** 分页 */
18
- pageMode: 'pagination' | 'read-more';
19
- pageSize: number;
18
+ private _flatMode?;
19
+ get flatMode(): boolean;
20
+ set flatMode(val: boolean);
21
+ /** 分页方式 */
22
+ _pageMode?: 'pagination' | 'read-more';
23
+ get pageMode(): 'pagination' | 'read-more';
24
+ set pageMode(val: 'pagination' | 'read-more');
25
+ /** 每页数量 (每次请求获取量) */
26
+ private _pageSize?;
27
+ get pageSize(): number;
28
+ set pageSize(val: number);
20
29
  scrollListenerAt?: HTMLElement;
21
30
  repositionAt?: HTMLElement;
22
31
  protected pagination?: Pagination;
@@ -35,6 +44,7 @@ export default class ListLite extends Component {
35
44
  setLoading(val: boolean, isFirstLoad?: boolean): void;
36
45
  /** 评论获取 */
37
46
  fetchComments(offset: number): Promise<void>;
47
+ private confLoaded;
38
48
  protected onLoad(data: ListData, offset: number): void;
39
49
  /** 分页模式 */
40
50
  private refreshPagination;
@@ -20,6 +20,7 @@ export default class List extends ListLite {
20
20
  /** 管理员设置页面信息 */
21
21
  adminPageEditSave(): void;
22
22
  showUnreadBadge(count: number): void;
23
+ private dropdownLoaded;
23
24
  /** 初始化选择下拉层 */
24
25
  protected initDropdown(): void;
25
26
  updateUnread(notifies: NotifyData[]): void;
@@ -1,4 +1,5 @@
1
1
  import { I18n } from '../src/i18n'
2
+ import { CommentData } from './artalk-data'
2
3
 
3
4
  export default interface ArtalkConfig {
4
5
  /** 装载元素 */
@@ -39,6 +40,9 @@ export default interface ArtalkConfig {
39
40
  default: string
40
41
  }
41
42
 
43
+ /** 头像链接生成器 */
44
+ avatarURLBuilder?: (comment: CommentData) => string,
45
+
42
46
  /** 分页配置 */
43
47
  pagination: {
44
48
  /** 每次请求获取数量 */
@@ -103,7 +107,7 @@ export default interface ArtalkConfig {
103
107
  /** 版本检测 */
104
108
  versionCheck: boolean
105
109
 
106
- /** 应用后端配置 */
110
+ /** 引用后端配置 */
107
111
  useBackendConf: boolean
108
112
 
109
113
  /** 语言本地化 */
@@ -159,6 +159,29 @@ export interface UserData {
159
159
 
160
160
  /** 是否属于管理员 */
161
161
  is_admin: boolean
162
+
163
+ /** 所属站点名 */
164
+ site_names: string[]
165
+
166
+ /** 所属站点名 (字符串) */
167
+ site_names_raw: string,
168
+
169
+ /** 是否允许接收邮件通知 */
170
+ receive_email: boolean
171
+ }
172
+
173
+ export interface UserDataForAdmin extends UserData {
174
+ /** 最后一次操作 IP */
175
+ last_ip: string
176
+
177
+ /** 最后一次操作 UA */
178
+ last_ua: string
179
+
180
+ /** 是否存在于配置文件中 */
181
+ is_in_conf: boolean
182
+
183
+ /** 评论数 */
184
+ comment_count: number
162
185
  }
163
186
 
164
187
  export interface NotifyData {
@@ -143,4 +143,7 @@ export default interface ContextApi {
143
143
 
144
144
  /** 设置暗黑模式 */
145
145
  setDarkMode(darkMode: boolean): void
146
+
147
+ /** 更新配置 */
148
+ updateConf(conf: Partial<ArtalkConfig>): void
146
149
  }
@@ -9,7 +9,7 @@ export interface EventPayloadMap {
9
9
  'editor-submit': undefined // 编辑器提交时
10
10
  'editor-submitted': undefined // 编辑器提交后
11
11
  'user-changed': LocalUser // 本地用户数据变更时
12
- 'conf-updated': undefined // Artalk 配置变更时
12
+ 'conf-loaded': undefined // Artalk 配置变更时
13
13
  'sidebar-show': undefined // 侧边栏显示
14
14
  'sidebar-hide': undefined // 侧边栏隐藏
15
15
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "artalk",
3
- "version": "2.4.2",
4
- "backendMinVersion": "2.2.0",
3
+ "version": "2.4.4",
4
+ "backendMinVersion": "2.3.0",
5
5
  "license": "LGPL-3.0",
6
6
  "description": "A Selfhosted Comment System",
7
7
  "keywords": [
@@ -30,7 +30,7 @@
30
30
  "hanabi": "^0.4.0",
31
31
  "insane": "^2.6.2",
32
32
  "marked": "^4.0.19",
33
- "abortcontroller-polyfill": "^1.7.3"
33
+ "abortcontroller-polyfill": "^1.7.5"
34
34
  },
35
35
  "scripts": {
36
36
  "dev": "vite",
package/src/api/index.ts CHANGED
@@ -18,11 +18,12 @@ const ApiComponents = {
18
18
 
19
19
  class Api {
20
20
  protected ctx: Context
21
- public baseURL: string
21
+ public get baseURL() {
22
+ return `${this.ctx.conf.server}/api`
23
+ }
22
24
 
23
25
  constructor (ctx: Context) {
24
26
  this.ctx = ctx
25
- this.baseURL = `${ctx.conf.server}/api`
26
27
 
27
28
  Object.entries(ApiComponents).forEach(([key, ApiComponent]) => {
28
29
  this[key] = new ApiComponent(this, this.ctx)
package/src/api/system.ts CHANGED
@@ -36,4 +36,11 @@ export default class SystemApi extends ApiBase {
36
36
  })
37
37
  return data
38
38
  }
39
+
40
+ /** 获取 API 版本信息 */
41
+ public async version() {
42
+ const resp = await fetch(`${this.api.baseURL}/version`, { method: 'POST' })
43
+ const data = await resp.json()
44
+ return data as { app: string, version: string, commit_hash: string, fe_min_version: string }
45
+ }
39
46
  }
package/src/api/user.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { UserData, NotifyData } from '~/types/artalk-data'
1
+ import { UserData, NotifyData, UserDataForAdmin } from '~/types/artalk-data'
2
2
  import ApiBase from './api-base'
3
3
  import { ToFormData } from './request'
4
4
 
@@ -15,7 +15,7 @@ export default class UserApi extends ApiBase {
15
15
  if (this.ctx.conf.site) params.site_name = this.ctx.conf.site
16
16
 
17
17
  const data = await this.POST<any>('/login', params)
18
- return (data.token as string)
18
+ return data as { token: string, user: UserData }
19
19
  }
20
20
 
21
21
  /** 用户 · 获取 */
@@ -72,4 +72,61 @@ export default class UserApi extends ApiBase {
72
72
 
73
73
  return this.POST(`/mark-read`, params)
74
74
  }
75
+
76
+ /** 用户 · 列表 */
77
+ public async userList(offset?: number, limit?: number, type?: 'all'|'admin'|'in_conf') {
78
+ const params: any = {
79
+ offset: offset || 0,
80
+ limit: limit || 15,
81
+ }
82
+
83
+ if (type) params.type = type
84
+
85
+ const d = await this.POST<any>('/admin/user-get', params)
86
+ return (d as { users: UserDataForAdmin[], total: number })
87
+ }
88
+
89
+ /** 用户 · 新增 */
90
+ public async userAdd(user: Partial<UserDataForAdmin>, password?: string) {
91
+ const params: any = {
92
+ name: user.name || '',
93
+ email: user.email || '',
94
+ password: password || '',
95
+ link: user.link || '',
96
+ is_admin: user.is_admin || false,
97
+ site_names: user.site_names_raw || '',
98
+ receive_email: user.receive_email || true,
99
+ badge_name: user.badge_name || '',
100
+ badge_color: user.badge_color || '',
101
+ }
102
+
103
+ const d = await this.POST<any>('/admin/user-add', params)
104
+ return (d.user as UserDataForAdmin)
105
+ }
106
+
107
+ /** 用户 · 修改 */
108
+ public async userEdit(user: Partial<UserDataForAdmin>, password?: string) {
109
+ const params: any = {
110
+ id: user.id,
111
+ name: user.name || '',
112
+ email: user.email || '',
113
+ password: password || '',
114
+ link: user.link || '',
115
+ is_admin: user.is_admin || false,
116
+ site_names: user.site_names_raw || '',
117
+ receive_email: user.receive_email || true,
118
+ badge_name: user.badge_name || '',
119
+ badge_color: user.badge_color || '',
120
+ }
121
+
122
+ const d = await this.POST<any>('/admin/user-edit', params)
123
+ return (d.user as UserDataForAdmin)
124
+ }
125
+
126
+ /** 用户 · 删除 */
127
+ public userDel(userID: number) {
128
+ return this.POST('/admin/user-del', {
129
+ id: String(userID)
130
+ })
131
+ }
75
132
  }
package/src/artalk.ts CHANGED
@@ -48,9 +48,6 @@ export default class Artalk {
48
48
  /* 初始化 Context */
49
49
  this.ctx = new ConcreteContext(this.conf, this.$root)
50
50
 
51
- // @ts-ignore 远程加载引用后端的配置
52
- if (this.conf.useBackendConf) return this.loadConfRemoteAndInitComponents()
53
-
54
51
  /* 初始化组件 */
55
52
  this.initComponents()
56
53
  }
@@ -59,7 +56,6 @@ export default class Artalk {
59
56
  private initComponents() {
60
57
  this.initLocale()
61
58
  this.initLayer()
62
- this.initDarkMode()
63
59
  Utils.initMarked(this.ctx)
64
60
 
65
61
  const Components: { [name: string]: () => void } = {
@@ -91,7 +87,6 @@ export default class Artalk {
91
87
  sidebarLayer: () => {
92
88
  const sidebarLayer = new SidebarLayer(this.ctx)
93
89
  this.ctx.setSidebarLayer(sidebarLayer)
94
- this.$root.appendChild(sidebarLayer.$el)
95
90
  },
96
91
 
97
92
  // 默认事件绑定
@@ -148,24 +143,6 @@ export default class Artalk {
148
143
  return conf
149
144
  }
150
145
 
151
- /** 初始化组件根据远程加载的配置 */
152
- private async loadConfRemoteAndInitComponents() {
153
- await this.loadConfRemote()
154
- this.initComponents()
155
- return this
156
- }
157
-
158
- /** 远程加载配置 */
159
- private async loadConfRemote() {
160
- Ui.showLoading(this.$root)
161
- let backendConf = {}
162
- try {
163
- backendConf = await this.ctx.getApi().system.conf()
164
- } catch (err) { console.error("Load config from remote err", err) }
165
- this.ctx.conf = Utils.mergeDeep(this.ctx.conf, backendConf)
166
- Ui.hideLoading(this.$root)
167
- }
168
-
169
146
  /** 事件绑定初始化 */
170
147
  private initEventBind() {
171
148
  // 锚点快速跳转评论
@@ -194,18 +171,6 @@ export default class Artalk {
194
171
  Layer.BodyOrgPaddingRight = document.body.style.paddingRight
195
172
  }
196
173
 
197
- /** 暗黑模式初始化 */
198
- private initDarkMode() {
199
- if (this.conf.darkMode === 'auto') {
200
- // 自动切换暗黑模式,事件监听
201
- const darkModeMedia = window.matchMedia('(prefers-color-scheme: dark)')
202
- darkModeMedia.addEventListener('change', (e) => { this.setDarkMode(e.matches) })
203
- this.setDarkMode(darkModeMedia.matches)
204
- } else {
205
- this.setDarkMode(this.conf.darkMode || false)
206
- }
207
- }
208
-
209
174
  /** 监听事件 */
210
175
  public on<K extends keyof EventPayloadMap>(name: K, handler: Handler<EventPayloadMap[K]>) {
211
176
  this.ctx.on(name, handler, 'external')
@@ -58,7 +58,8 @@ export default class CommentRender {
58
58
  private renderAvatar() {
59
59
  const $avatar = this.$el.querySelector<HTMLElement>('.atk-avatar')!
60
60
  const $avatarImg = Utils.createElement<HTMLImageElement>('<img />')
61
- $avatarImg.src = this.comment.getGravatarURL()
61
+ const avatarURLBuilder = this.ctx.conf.avatarURLBuilder
62
+ $avatarImg.src = avatarURLBuilder ? avatarURLBuilder(this.data) : this.comment.getGravatarURL()
62
63
  if (this.data.link) {
63
64
  const $avatarA = Utils.createElement<HTMLLinkElement>('<a target="_blank" rel="noreferrer noopener nofollow"></a>')
64
65
  $avatarA.href = Utils.isValidURL(this.data.link) ? this.data.link : `https://${this.data.link}`
package/src/context.ts CHANGED
@@ -4,6 +4,7 @@ import { CommentData, NotifyData } from '~/types/artalk-data'
4
4
  import { Event } from '~/types/event'
5
5
  import getI18n, { I18n } from './i18n'
6
6
  import User from './lib/user'
7
+ import * as Utils from './lib/utils'
7
8
  import ContextApi from '../types/context'
8
9
  import Editor from './editor'
9
10
  import Comment from './comment'
@@ -49,6 +50,10 @@ export default class Context implements ContextApi {
49
50
  this.$root.innerHTML = ''
50
51
 
51
52
  this.api = new Api(this)
53
+
54
+ this.on('conf-loaded', () => {
55
+ this.refreshDarkModeConf()
56
+ })
52
57
  }
53
58
 
54
59
  /* 设置持有的同事类 */
@@ -261,16 +266,39 @@ export default class Context implements ContextApi {
261
266
  const darkModeClassName = 'atk-dark-mode'
262
267
 
263
268
  this.conf.darkMode = darkMode
264
- this.trigger('conf-updated')
265
269
 
266
- if (this.conf.darkMode) this.$root.classList.add(darkModeClassName)
270
+ if (darkMode) this.$root.classList.add(darkModeClassName)
267
271
  else this.$root.classList.remove(darkModeClassName)
268
272
 
269
273
  // for Layer
270
274
  const { $wrap: $layerWrap } = GetLayerWrap(this)
271
275
  if ($layerWrap) {
272
- if (this.conf.darkMode) $layerWrap.classList.add(darkModeClassName)
276
+ if (darkMode) $layerWrap.classList.add(darkModeClassName)
273
277
  else $layerWrap.classList.remove(darkModeClassName)
274
278
  }
275
279
  }
280
+
281
+ private darkModeMedia = window.matchMedia('(prefers-color-scheme: dark)')
282
+ private darkModeAutoFunc?: (evt: MediaQueryListEvent) => void
283
+ private refreshDarkModeConf(): void {
284
+ if (this.conf.darkMode === 'auto') {
285
+ // 自动切换暗黑模式,事件监听
286
+ this.setDarkMode(this.darkModeMedia.matches)
287
+ if (!this.darkModeAutoFunc) {
288
+ this.darkModeAutoFunc = (evt) => { this.setDarkMode(evt.matches) }
289
+ this.darkModeMedia.addEventListener('change', this.darkModeAutoFunc)
290
+ }
291
+ } else {
292
+ if (this.darkModeAutoFunc) {
293
+ // 解除事件监听绑定
294
+ this.darkModeMedia.removeEventListener('change', this.darkModeAutoFunc)
295
+ }
296
+ this.setDarkMode(this.conf.darkMode || false)
297
+ }
298
+ }
299
+
300
+ public updateConf(conf: Partial<ArtalkConfig>): void {
301
+ this.conf = Utils.mergeDeep(this.conf, conf)
302
+ this.trigger('conf-loaded')
303
+ }
276
304
  }
package/src/defaults.ts CHANGED
@@ -29,7 +29,7 @@ const defaults: ArtalkConfig = {
29
29
 
30
30
  gravatar: {
31
31
  default: 'mp',
32
- mirror: 'https://sdn.geekzu.org/avatar/',
32
+ mirror: 'https://cravatar.cn/avatar/',
33
33
  },
34
34
 
35
35
  pagination: {
@@ -46,7 +46,7 @@ const defaults: ArtalkConfig = {
46
46
  imgUpload: true,
47
47
  reqTimeout: 15000,
48
48
  versionCheck: true,
49
- useBackendConf: false,
49
+ useBackendConf: true,
50
50
 
51
51
  locale: 'zh-CN',
52
52
  }
@@ -68,15 +68,15 @@ export default class Editor extends Component {
68
68
  this.$plugBtnWrap = this.$el.querySelector('.atk-plug-btn-wrap')!
69
69
  this.$plugPanelWrap = this.$el.querySelector('.atk-plug-panel-wrap')!
70
70
 
71
- // 执行初始化
72
- this.initLocalStorage()
73
- this.initHeader()
74
- this.initTextarea()
75
- this.initPlugs()
76
- this.initSubmitBtn()
77
-
78
71
  // 监听事件
79
- this.ctx.on('conf-updated', () => {})
72
+ this.ctx.on('conf-loaded', () => {
73
+ // 执行初始化
74
+ this.initLocalStorage()
75
+ this.initHeader()
76
+ this.initTextarea()
77
+ this.initSubmitBtn()
78
+ this.initPlugs()
79
+ })
80
80
  }
81
81
 
82
82
  private initLocalStorage() {
@@ -433,6 +433,7 @@ export default class Editor extends Component {
433
433
  this.$plugBtnWrap.innerHTML = ''
434
434
 
435
435
  const disabledPlugs: string[] = []
436
+ if (!this.conf.imgUpload) disabledPlugs.push('upload')
436
437
  if (!this.conf.emoticons) disabledPlugs.push('emoticons')
437
438
  if (!this.conf.preview) disabledPlugs.push('preview')
438
439
 
@@ -33,7 +33,7 @@ export default class UploadPlug extends EditorPlug {
33
33
  $input.click() // 显示选择图片对话框
34
34
  }
35
35
 
36
- this.ctx.on('conf-updated', () => {
36
+ this.ctx.on('conf-loaded', () => {
37
37
  if (!this.ctx.conf.imgUpload) {
38
38
  this.getBtn()!.setAttribute('atk-only-admin-show', '')
39
39
  this.ctx.checkAdminShowEl()
@@ -53,11 +53,6 @@ export default class SidebarLayer extends Component {
53
53
  // viewWrap 滚动条归位
54
54
  // this.$viewWrap.scrollTo(0, 0)
55
55
 
56
- // 执行动画
57
- setTimeout(() => {
58
- this.$el.style.transform = 'translate(0, 0)'
59
- }, 20)
60
-
61
56
  // 管理员身份验证 (若身份失效,弹出验证窗口)
62
57
  ;(async () => {
63
58
  const resp = await this.ctx.getApi().user.loginStatus()
@@ -76,9 +71,6 @@ export default class SidebarLayer extends Component {
76
71
  }
77
72
  })()
78
73
 
79
- // 清空 unread
80
- this.ctx.updateNotifies([])
81
-
82
74
  // 第一次加载
83
75
  if (this.firstShow) {
84
76
  this.$iframeWrap.innerHTML = ''
@@ -114,6 +106,16 @@ export default class SidebarLayer extends Component {
114
106
  this.iframeLoad(this.$iframe!.src.replace('&darkMode=1', ''))
115
107
  }
116
108
 
109
+ // 执行滑动显示动画
110
+ setTimeout(() => {
111
+ this.$el.style.transform = 'translate(0, 0)'
112
+ }, 100)
113
+
114
+ // 清空 unread
115
+ setTimeout(() => {
116
+ this.ctx.updateNotifies([])
117
+ }, 0)
118
+
117
119
  this.ctx.trigger('sidebar-show')
118
120
  }
119
121
 
@@ -11,7 +11,10 @@ const AdminChecker: Checker = {
11
11
  password: inputVal
12
12
  }
13
13
 
14
- return that.ctx.getApi().user.login(data.name, data.email, data.password)
14
+ return (async () => {
15
+ const resp = await that.ctx.getApi().user.login(data.name, data.email, data.password)
16
+ return resp.token
17
+ })()
15
18
  },
16
19
 
17
20
  body(that, ctx) {
@@ -77,6 +77,7 @@ const CaptchaChecker: Checker = {
77
77
 
78
78
  onError(that, ctx, err, inputVal, formEl) {
79
79
  formEl.querySelector<HTMLElement>('.atk-captcha-img')!.click() // 刷新验证码
80
+ formEl.querySelector<HTMLInputElement>('input[type="text"]')!.value = '' // 清空输入框输入
80
81
  }
81
82
  }
82
83
 
package/src/lib/stat.ts CHANGED
@@ -9,7 +9,15 @@ export interface CountConf {
9
9
  }
10
10
 
11
11
  export const PvCountWidget: ArtalkPlug = (ctx: ContextApi) => {
12
- initCountWidget({ ctx, pvAdd: true })
12
+ if (!ctx.conf.useBackendConf) {
13
+ // 不使用后端配置,在 Artalk 实例被创建后,立刻初始化
14
+ initCountWidget({ ctx, pvAdd: true })
15
+ } else {
16
+ // 若使用后端配置,需待配置成功获取后 (来自后端设定的 pvEl 等),再初始化
17
+ ctx.on('list-loaded', () => {
18
+ initCountWidget({ ctx, pvAdd: true })
19
+ })
20
+ }
13
21
  }
14
22
 
15
23
  /** 初始化评论数和 PV 数量展示元素 */
package/src/lib/utils.ts CHANGED
@@ -204,7 +204,14 @@ export function marked(ctx: Context, src: string): string {
204
204
  // @link https://gist.github.com/lionel-rowe/bb384465ba4e4c81a9c8dada84167225
205
205
  let dest = insane(markedContent, {
206
206
  allowedClasses: {},
207
- allowedSchemes: ['http', 'https', 'mailto'],
207
+ // @refer CVE-2018-8495
208
+ // @link https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-8495
209
+ // @link https://leucosite.com/Microsoft-Edge-RCE/
210
+ // @link https://medium.com/@knownsec404team/analysis-of-the-security-issues-of-url-scheme-in-pc-from-cve-2018-8495-934478a36756
211
+ allowedSchemes: [
212
+ 'http', 'https', 'mailto',
213
+ 'data' // for support base64 encoded image (安全性有待考虑)
214
+ ],
208
215
  allowedTags: [
209
216
  'a', 'abbr', 'article', 'b', 'blockquote', 'br', 'caption', 'code', 'del', 'details', 'div', 'em',
210
217
  'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'i', 'img', 'ins', 'kbd', 'li', 'main', 'mark',