artalk 2.2.3 → 2.2.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.
package/index.html CHANGED
@@ -32,6 +32,13 @@
32
32
  <!--<link rel="stylesheet"
33
33
  href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/11.5.1/styles/arta.min.css">
34
34
  <script src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/11.5.1/highlight.min.js"></script>-->
35
+
36
+ <!-- For mobile testing -->
37
+ <script src="https://unpkg.com/vconsole@latest/dist/vconsole.min.js"></script>
38
+ <script>
39
+ // VConsole will be exported to `window.VConsole` by default.
40
+ var vConsole = new window.VConsole();
41
+ </script>
35
42
  </head>
36
43
  <body>
37
44
  <div class="card billboard atk-fade-in">
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "artalk",
3
- "version": "2.2.3",
4
- "backendMinVersion": "2.1.1",
3
+ "version": "2.2.4",
4
+ "backendMinVersion": "2.1.2",
5
5
  "license": "LGPL-3.0",
6
6
  "description": "A Selfhosted Comment System",
7
7
  "keywords": [
package/src/api/index.ts CHANGED
@@ -119,8 +119,11 @@ export default class Api {
119
119
 
120
120
  /** 用户 · 登录状态 */
121
121
  public async loginStatus() {
122
- const data = await POST<any>(this.ctx, `${this.baseURL}/login-status`)
123
- return (data || { is_login: false }) as { is_login: boolean }
122
+ const data = await POST<any>(this.ctx, `${this.baseURL}/login-status`, {
123
+ name: this.ctx.user.data.nick,
124
+ email: this.ctx.user.data.email
125
+ })
126
+ return (data || { is_login: false, is_admin: false }) as { is_login: boolean, is_admin: boolean }
124
127
  }
125
128
 
126
129
  // ============================
@@ -164,13 +167,14 @@ export default class Api {
164
167
  }
165
168
 
166
169
  /** 页面 · 数据更新 */
167
- public async pageFetch(id: number) {
168
- const params: any = {
169
- id,
170
- }
170
+ public async pageFetch(id?: number, siteName?: string, getStatus?: boolean) {
171
+ const params: any = {}
172
+ if (id) params.id = id
173
+ if (siteName) params.site_name = siteName
174
+ if (getStatus) params.get_status = getStatus
171
175
 
172
176
  const d = await POST<any>(this.ctx, `${this.baseURL}/admin/page-fetch`, params)
173
- return (d.page as PageData)
177
+ return (d as any)
174
178
  }
175
179
 
176
180
  // ============================
package/src/artalk.ts CHANGED
@@ -9,7 +9,7 @@ import Editor from './components/editor'
9
9
  import List from './components/list'
10
10
  import SidebarLayer from './components/sidebar-layer'
11
11
 
12
- import { GetLayerWrap } from './components/layer'
12
+ import Layer, { GetLayerWrap } from './components/layer'
13
13
  import { EventPayloadMap, Handler } from '~/types/event'
14
14
  import Api from './api'
15
15
  import * as Utils from './lib/utils'
@@ -37,6 +37,7 @@ export default class Artalk {
37
37
 
38
38
  // 默认 pageKey
39
39
  if (!this.conf.pageKey) {
40
+ // @link http://bl.ocks.org/abernier/3070589
40
41
  this.conf.pageKey = `${window.location.protocol}//${window.location.host}${window.location.pathname}`
41
42
  }
42
43
 
@@ -56,6 +57,7 @@ export default class Artalk {
56
57
  // 界面初始化
57
58
  this.$root.classList.add('artalk')
58
59
  this.$root.innerHTML = ''
60
+ this.initLayer()
59
61
  this.initDarkMode()
60
62
 
61
63
  // 组件初始化
@@ -133,6 +135,13 @@ export default class Artalk {
133
135
  this.list.fetchComments(0)
134
136
  }
135
137
 
138
+ /** Layer 初始化 */
139
+ initLayer() {
140
+ // 记录页面原始 Styles
141
+ Layer.BodyOrgOverflow = document.body.style.overflow
142
+ Layer.BodyOrgPaddingRight = document.body.style.paddingRight
143
+ }
144
+
136
145
  /** 暗黑模式 · 初始化 */
137
146
  public initDarkMode() {
138
147
  const darkModeClassName = 'atk-dark-mode'
@@ -163,14 +172,15 @@ export default class Artalk {
163
172
 
164
173
  /** PV */
165
174
  public async initPV() {
175
+ const pvNum = await new Api(this.ctx).pv()
176
+ if (Number.isNaN(Number(pvNum)))
177
+ return
178
+
166
179
  if (!this.conf.pvEl || !document.querySelector(this.conf.pvEl))
167
180
  return
168
181
 
169
182
  const $pv = document.querySelector<HTMLElement>(this.conf.pvEl)!
170
183
  // $pv.innerText = '-' // 默认占位符
171
- const pvNum = await new Api(this.ctx).pv()
172
- if (Number.isNaN(Number(pvNum)))
173
- return
174
184
 
175
185
  $pv.innerText = String(pvNum)
176
186
  }
@@ -22,6 +22,7 @@ export default class Editor extends Component {
22
22
  public $bottom: HTMLElement
23
23
  public $plugBtnWrap: HTMLElement
24
24
  public $imgUploadBtn?: HTMLElement
25
+ public $imgUploadInput?: HTMLInputElement
25
26
  public $submitBtn: HTMLButtonElement
26
27
  public $notifyWrap: HTMLElement
27
28
 
@@ -265,12 +266,16 @@ export default class Editor extends Component {
265
266
  this.$imgUploadBtn = Utils.createElement(`<span class="atk-plug-btn">图片</span>`)
266
267
  this.$plugBtnWrap.querySelector('[data-plug-name="preview"]')!.before(this.$imgUploadBtn) // 显示在预览图标之前
267
268
 
269
+ this.$imgUploadInput = document.createElement('input')
270
+ this.$imgUploadInput.type = 'file'
271
+ this.$imgUploadInput.style.display = 'none'
272
+ this.$imgUploadInput.accept = this.allowImgExts.map(o => `.${o}`).join(',')
273
+ this.$imgUploadBtn.after(this.$imgUploadInput)
274
+
268
275
  // 按钮点击
269
276
  this.$imgUploadBtn.onclick = () => {
270
277
  // 选择图片
271
- const $input = document.createElement('input')
272
- $input.type = 'file'
273
- $input.accept = this.allowImgExts.map(o => `.${o}`).join(',')
278
+ const $input = this.$imgUploadInput!
274
279
  $input.onchange = () => {
275
280
  (async () => { // 解决阻塞 UI 问题
276
281
  if (!$input.files || $input.files.length === 0) return
@@ -501,7 +506,7 @@ export default class Editor extends Component {
501
506
 
502
507
  // 回复不同页面的评论
503
508
  if (this.replyComment !== null && this.replyComment.page_key !== this.ctx.conf.pageKey) {
504
- window.open(`${this.replyComment.page_key}#atk-comment-${nComment.id}`)
509
+ window.open(`${this.replyComment.page_url}#atk-comment-${nComment.id}`)
505
510
  }
506
511
 
507
512
  this.ctx.trigger('list-insert', nComment)
@@ -10,8 +10,8 @@ export default class Layer extends Component {
10
10
 
11
11
  private maskClickHideEnable: boolean = true
12
12
 
13
- private bodyStyleOrgOverflow = ''
14
- private bodyStyleOrgPaddingRight = ''
13
+ public static BodyOrgOverflow: string
14
+ public static BodyOrgPaddingRight: string
15
15
 
16
16
  public afterHide?: Function
17
17
 
@@ -73,10 +73,11 @@ export default class Layer extends Component {
73
73
  this.$wrap.classList.add('atk-fade-out')
74
74
  this.$el.style.display = 'none'
75
75
 
76
+ // body style 禁止滚动解除
77
+ this.pageBodyScrollBarShow()
78
+
76
79
  this.newActionTimer(() => {
77
80
  this.$wrap.style.display = 'none'
78
- // body style 禁止滚动解除
79
- this.pageBodyScrollBarShow()
80
81
  this.checkCleanLayer()
81
82
  }, 450)
82
83
  this.newActionTimer(() => {
@@ -91,8 +92,6 @@ export default class Layer extends Component {
91
92
 
92
93
  // 页面滚动条隐藏
93
94
  pageBodyScrollBarHide() {
94
- this.bodyStyleOrgOverflow = document.body.style.overflow
95
- this.bodyStyleOrgPaddingRight = document.body.style.paddingRight
96
95
  document.body.style.overflow = 'hidden'
97
96
 
98
97
  const bpr = parseInt(window.getComputedStyle(document.body, null).getPropertyValue('padding-right'), 10)
@@ -101,8 +100,8 @@ export default class Layer extends Component {
101
100
 
102
101
  // 页面滚动条显示
103
102
  pageBodyScrollBarShow() {
104
- document.body.style.overflow = this.bodyStyleOrgOverflow
105
- document.body.style.paddingRight = this.bodyStyleOrgPaddingRight
103
+ document.body.style.overflow = Layer.BodyOrgOverflow
104
+ document.body.style.paddingRight = Layer.BodyOrgPaddingRight
106
105
  }
107
106
 
108
107
  // Timers
@@ -128,7 +127,6 @@ export default class Layer extends Component {
128
127
 
129
128
  /** 销毁 - 无动画 */
130
129
  disposeNow () {
131
- document.body.style.overflow = ''
132
130
  this.$el.remove()
133
131
  this.pageBodyScrollBarShow()
134
132
  // this.$el dispose
@@ -91,7 +91,7 @@ export default class ListLite extends Component {
91
91
  try {
92
92
  listData = await new Api(this.ctx).get(offset, this.pageSize, this.flatMode, this.paramsEditor)
93
93
  } catch (e: any) {
94
- this.onError(e.msg || String(e), offset)
94
+ this.onError(e.msg || String(e), offset, e.data)
95
95
  throw e
96
96
  } finally {
97
97
  hideLoading()
@@ -219,7 +219,7 @@ export default class ListLite extends Component {
219
219
  }
220
220
 
221
221
  /** 错误处理 */
222
- protected onError(msg: any, offset: number) {
222
+ protected onError(msg: any, offset: number, errData?: any) {
223
223
  msg = String(msg)
224
224
  console.error(msg)
225
225
 
@@ -237,9 +237,24 @@ export default class ListLite extends Component {
237
237
  $err.appendChild($retryBtn)
238
238
 
239
239
  const adminBtn = Utils.createElement('<span atk-only-admin-show> | <span style="cursor:pointer;">打开控制台</span></span>')
240
- adminBtn.onclick = () => (this.ctx.trigger('sidebar-show'))
241
- if (!this.ctx.user.data.isAdmin) adminBtn.classList.add('atk-hide')
242
240
  $err.appendChild(adminBtn)
241
+ if (!this.ctx.user.data.isAdmin) adminBtn.classList.add('atk-hide')
242
+
243
+ let sidebarView = ''
244
+
245
+ // 找不到站点错误,打开侧边栏并填入创建站点表单
246
+ if (errData?.err_no_site) {
247
+ const viewLoadParam = {
248
+ create_name: this.ctx.conf.site,
249
+ create_urls: `${window.location.protocol}//${window.location.host}`
250
+ }
251
+ // TODO 真的是飞鸽传书啊
252
+ sidebarView = `sites|${JSON.stringify(viewLoadParam)}`
253
+ }
254
+
255
+ adminBtn.onclick = () => (this.ctx.trigger('sidebar-show', {
256
+ view: sidebarView
257
+ }))
243
258
 
244
259
  Ui.setError(this.$el, $err)
245
260
  }
@@ -7,6 +7,7 @@ import SidebarHTML from './html/sidebar-layer.html?raw'
7
7
  import * as Ui from '@/lib/ui'
8
8
  import Layer from './layer'
9
9
  import Api from '../api'
10
+ import { SidebarShowPayload } from '~/types/event'
10
11
 
11
12
  export default class SidebarLayer extends Component {
12
13
  public layer?: Layer
@@ -28,7 +29,7 @@ export default class SidebarLayer extends Component {
28
29
  }
29
30
 
30
31
  // event
31
- this.ctx.on('sidebar-show', () => (this.show()))
32
+ this.ctx.on('sidebar-show', (conf) => (this.show(conf || {})))
32
33
  this.ctx.on('sidebar-hide', () => (this.hide()))
33
34
  this.ctx.on('user-changed', () => { this.firstShow = true })
34
35
  }
@@ -36,7 +37,7 @@ export default class SidebarLayer extends Component {
36
37
  private firstShow = true
37
38
 
38
39
  /** 显示 */
39
- public async show() {
40
+ public async show(conf: SidebarShowPayload) {
40
41
  this.$el.style.transform = '' // 动画清除,防止二次打开失效
41
42
 
42
43
  // 获取 Layer
@@ -59,54 +60,60 @@ export default class SidebarLayer extends Component {
59
60
  this.$el.style.transform = 'translate(0, 0)'
60
61
  }, 20)
61
62
 
62
- // 第一次加载
63
- if (this.firstShow) {
64
- // 管理员身份验证
65
- if (this.ctx.user.data.isAdmin) {
66
- const resp = await new Api(this.ctx).loginStatus()
67
- if (!resp.is_login) {
68
- await (new Promise<any>((resolve, reject) => {
69
- this.ctx.trigger('checker-admin', {
70
- onSuccess: () => {
71
- resolve(null)
72
- setTimeout(() => {
73
- this.show()
74
- }, 500)
75
- },
76
- onCancel: () => {
77
- this.layer?.hide()
78
- }
79
- })
80
- }))
81
- }
63
+ // 管理员身份验证 (若身份失效,弹出验证窗口)
64
+ ;(async () => {
65
+ const resp = await new Api(this.ctx).loginStatus()
66
+ if (resp.is_admin && !resp.is_login) {
67
+ this.layer?.hide()
68
+ this.firstShow = true
69
+
70
+ this.ctx.trigger('checker-admin', {
71
+ onSuccess: () => {
72
+ setTimeout(() => {
73
+ this.show(conf)
74
+ }, 500)
75
+ },
76
+ onCancel: () => {}
77
+ })
82
78
  }
79
+ })()
80
+
81
+ // 清空 unread
82
+ this.ctx.trigger('unread-update', { notifies: [] })
83
83
 
84
+ // 第一次加载
85
+ if (this.firstShow) {
84
86
  this.$iframeWrap.innerHTML = ''
85
87
  this.$iframe = Utils.createElement<HTMLIFrameElement>('<iframe></iframe>')
86
88
 
89
+ // 准备 Iframe 参数
87
90
  const baseURL = (import.meta.env.MODE === 'development') ? 'http://localhost:23367/'
88
91
  : Utils.getURLBasedOnApi(this.ctx, 'sidebar/')
89
- const userData = encodeURIComponent(JSON.stringify(this.ctx.user.data))
90
92
 
91
- const location = window.location
92
- this.iframeLoad(`${baseURL}`
93
- + `?pageKey=${encodeURIComponent(this.conf.pageKey)}`
94
- + `&site=${encodeURIComponent(this.conf.site || '')}`
95
- + `&user=${userData}`
96
- + `&time=${+new Date()}`
97
- + `&referer=${encodeURIComponent(`${location.protocol}//${location.host}${location.pathname}`)}`
98
- + `${((this.conf.darkMode) ? `&darkMode=1` : ``)}`)
93
+ const query: any = {
94
+ pageKey: this.conf.pageKey,
95
+ site: this.conf.site || '',
96
+ user: JSON.stringify(this.ctx.user.data),
97
+ time: +new Date()
98
+ }
99
+
100
+ if (conf.view) query.view = conf.view
101
+ if (this.conf.darkMode) query.darkMode = '1'
102
+
103
+ const urlParams = new URLSearchParams(query);
104
+ this.iframeLoad(`${baseURL}?${urlParams.toString()}`)
99
105
 
100
106
  this.$iframeWrap.append(this.$iframe)
101
107
  this.firstShow = false
102
108
  } else {
103
109
  // 暗黑模式
104
- if (this.conf.darkMode && !this.$iframe!.src.match(/darkMode=1$/)) {
110
+ const isIframeSrcDarkMode = this.$iframe!.src.includes('darkMode=1')
111
+
112
+ if (this.conf.darkMode && !isIframeSrcDarkMode)
105
113
  this.iframeLoad(`${this.$iframe!.src}&darkMode=1`)
106
- }
107
- if (!this.conf.darkMode && this.$iframe!.src.match(/darkMode=1$/)) {
108
- this.iframeLoad(this.$iframe!.src.replace(/&darkMode=1$/, ''))
109
- }
114
+
115
+ if (!this.conf.darkMode && isIframeSrcDarkMode)
116
+ this.iframeLoad(this.$iframe!.src.replace('&darkMode=1', ''))
110
117
  }
111
118
  }
112
119
 
@@ -15,7 +15,7 @@ const CaptchaChecker: Checker = {
15
15
  const $iframe = Utils.createElement<HTMLIFrameElement>(`<iframe class="atk-fade-in"></iframe>`)
16
16
  $iframe.style.display = 'none'
17
17
  Ui.showLoading($iframeWrap, { transparentBg: true })
18
- $iframe.src = `${that.ctx.conf.server}/captcha/get`
18
+ $iframe.src = `${that.ctx.conf.server}/captcha/get?t=${+new Date()}`
19
19
  $iframe.onload = () => {
20
20
  $iframe.style.display = ''
21
21
  Ui.hideLoading($iframeWrap)
@@ -167,6 +167,7 @@
167
167
  border-left: 3px solid #eee;
168
168
  margin-bottom: 10px;
169
169
  position: relative;
170
+ margin-top: 10px;
170
171
 
171
172
  .atk-meta {
172
173
  font-size: 15px;
@@ -19,9 +19,11 @@
19
19
 
20
20
  input, textarea, button, optgroup, select {
21
21
  font-family: inherit;
22
+ color: inherit;
23
+ font-size: inherit;
22
24
  }
23
25
 
24
- @codeFont: Source Code Pro,courier new,Input Mono,PT Mono,SFMono-Regular,Consolas,Monaco,Menlo,PingFang SC,Liberation Mono,Microsoft YaHei,Courier,monospace;
26
+ @codeFont: source code pro, Consolas, Monaco, Menlo, sans-serif;
25
27
 
26
28
  code {
27
29
  font-family: @codeFont;
@@ -33,8 +35,8 @@
33
35
  }
34
36
 
35
37
  pre {
36
- margin-top: 10px;
37
- margin-bottom: 0;
38
+ margin: 10px 0 0 0;
39
+ padding: 0;
38
40
  line-height: 0;
39
41
  }
40
42
 
@@ -196,6 +198,9 @@
196
198
  .atk-fade-out {
197
199
  animation: atkFadeOut both .2s;
198
200
  }
201
+ .atk-rotate {
202
+ animation: atkRotate 2s linear infinite;
203
+ }
199
204
 
200
205
  @keyframes atkFadeIn {
201
206
  0% { opacity: 0 }
@@ -207,6 +212,11 @@
207
212
  0% {}
208
213
  }
209
214
 
215
+ @keyframes atkRotate {
216
+ from { transform: rotate(0deg); }
217
+ to { transform: rotate(360deg); }
218
+ }
219
+
210
220
  i.atk-icon {
211
221
  width: 20px;
212
222
  height: 20px;
@@ -215,6 +225,7 @@ i.atk-icon {
215
225
  background-repeat: no-repeat;
216
226
  mask-repeat: no-repeat;
217
227
  mask-position: center;
228
+ mask-size: contain;
218
229
  }
219
230
 
220
231
  .atk-icon-sync {
package/types/event.d.ts CHANGED
@@ -19,7 +19,7 @@ export interface EventPayloadMap {
19
19
  'list-refresh-ui': undefined // 刷新 List UI
20
20
 
21
21
  // Sidebar
22
- 'sidebar-show': undefined // 侧边栏显示
22
+ 'sidebar-show': SidebarShowPayload // 侧边栏显示
23
23
  'sidebar-hide': undefined // 侧边栏隐藏
24
24
 
25
25
  // Editor
@@ -70,6 +70,10 @@ export interface EditorReplyPayload {
70
70
  scroll?: boolean
71
71
  }
72
72
 
73
+ export interface SidebarShowPayload {
74
+ view?: 'comments'|'sites'|'pages'|'transfer'|string
75
+ }
76
+
73
77
  // ============================================
74
78
  export interface Handler<P> {
75
79
  (payload: P): void