artalk 2.0.4 → 2.1.1

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 (91) hide show
  1. package/README.md +7 -60
  2. package/dist/Artalk.css +1 -1
  3. package/dist/Artalk.es.js +5996 -1
  4. package/dist/Artalk.iife.js +266 -1
  5. package/dist/Artalk.js +266 -1
  6. package/package.json +3 -4
  7. package/src/api/index.ts +269 -0
  8. package/src/api/request.ts +125 -0
  9. package/src/artalk.ts +153 -0
  10. package/src/components/_component.ts.txt +10 -0
  11. package/src/components/action-btn.ts +169 -0
  12. package/src/components/admin/page-list.ts +194 -0
  13. package/src/components/admin/site-list-floater.ts +98 -0
  14. package/src/components/admin/site-list.ts +289 -0
  15. package/src/components/comment.ts +478 -0
  16. package/src/components/dialog.ts +63 -0
  17. package/src/components/editor-plugs/{_ExamplePlug.ts.txt → _example-plug.ts.txt} +0 -0
  18. package/src/components/editor-plugs/{EditorPlug.ts → editor-plug.ts} +3 -3
  19. package/src/components/editor-plugs/{EmoticonsPlug.less → emoticons-plug.less} +0 -0
  20. package/src/components/editor-plugs/{EmoticonsPlug.ts → emoticons-plug.ts} +9 -9
  21. package/src/components/editor-plugs/{PreviewPlug.less → preview-plug.less} +0 -0
  22. package/src/components/editor-plugs/{PreviewPlug.ts → preview-plug.ts} +10 -10
  23. package/src/components/{Editor.ts → editor.ts} +104 -102
  24. package/src/components/html/comment.html +2 -6
  25. package/src/components/html/list.html +0 -1
  26. package/src/components/html/sidebar.html +18 -4
  27. package/src/components/item-text-editor.ts +136 -0
  28. package/src/components/layer.ts +135 -0
  29. package/src/components/list-lite.ts +507 -0
  30. package/src/components/list.ts +172 -0
  31. package/src/components/pagination.ts +150 -0
  32. package/src/components/read-more-btn.ts +71 -0
  33. package/src/components/sidebar-view.ts +26 -0
  34. package/src/components/sidebar-views/comments-view.ts +66 -0
  35. package/src/components/sidebar-views/pages-view.ts +61 -0
  36. package/src/components/sidebar-views/setting-view.ts +21 -0
  37. package/src/components/sidebar-views/sites-view.ts +42 -0
  38. package/src/components/sidebar-views/transfer-view.ts +218 -0
  39. package/src/components/sidebar.ts +288 -0
  40. package/src/context.ts +42 -0
  41. package/src/default-conf.ts +42 -0
  42. package/src/lib/checker/admin-checker.ts +33 -0
  43. package/src/lib/checker/captcha-checker.ts +38 -0
  44. package/src/lib/checker/index.ts +134 -0
  45. package/src/lib/component.ts +3 -3
  46. package/src/lib/md5.js +6 -0
  47. package/src/lib/ui.ts +64 -74
  48. package/src/lib/user.ts +2 -8
  49. package/src/lib/utils.ts +30 -11
  50. package/src/main.ts +1 -1
  51. package/src/style/_color.less +8 -2
  52. package/src/style/_extend.less +81 -0
  53. package/src/style/_variables.less +3 -0
  54. package/src/style/comment.less +60 -24
  55. package/src/style/editor.less +3 -0
  56. package/src/style/item-text-editor.less +35 -0
  57. package/src/style/list.less +51 -32
  58. package/src/style/main.less +75 -1
  59. package/src/style/page-list.less +50 -0
  60. package/src/style/pagination.less +58 -0
  61. package/src/style/sidebar.less +274 -199
  62. package/src/style/site-list.less +208 -0
  63. package/tsconfig.json +27 -0
  64. package/types/artalk-config.d.ts +68 -51
  65. package/types/artalk-data.d.ts +38 -1
  66. package/types/event.d.ts +73 -0
  67. package/types/index.d.ts +1 -6
  68. package/.github/workflows/npm.yml +0 -31
  69. package/.github/workflows/pages.yml +0 -30
  70. package/dist/src/Event.d.ts +0 -59
  71. package/dist/types/artalk-config.d.ts +0 -79
  72. package/dist/types/artalk-data.d.ts +0 -153
  73. package/dist/types/global.d.ts +0 -1
  74. package/dist/types/index.d.ts +0 -8
  75. package/dist/types/vite-env.d.ts +0 -1
  76. package/example/pjax/res1.html +0 -41
  77. package/example/pjax/res2.html +0 -41
  78. package/src/Artalk.ts +0 -160
  79. package/src/Constant.ts +0 -6
  80. package/src/Context.ts +0 -34
  81. package/src/Event.d.ts +0 -59
  82. package/src/components/Comment.ts +0 -402
  83. package/src/components/Layer.ts +0 -138
  84. package/src/components/List.ts +0 -162
  85. package/src/components/ListLite.ts +0 -407
  86. package/src/components/Sidebar.ts +0 -141
  87. package/src/components/sidebar-views/AdminView.ts +0 -523
  88. package/src/components/sidebar-views/MessageView.ts +0 -48
  89. package/src/components/sidebar-views/SidebarView.ts +0 -24
  90. package/src/lib/api.ts +0 -380
  91. package/src/lib/checker.ts +0 -164
@@ -0,0 +1,218 @@
1
+ import Api from '~/src/api'
2
+ import Context from '../../context'
3
+ import Component from '../../lib/component'
4
+ import * as Utils from '../../lib/utils'
5
+ import * as Ui from '../../lib/ui'
6
+ import SiteList from '../admin/site-list'
7
+ import Comment from '../comment'
8
+ import SidebarView from '../sidebar-view'
9
+
10
+ export default class TransferView extends SidebarView {ß
11
+ static viewName = 'transfer'
12
+ static viewTitle = '迁移'
13
+ static viewAdminOnly = true
14
+
15
+ viewTabs = {
16
+ 'import': '导入',
17
+ 'export': '导出',
18
+ }
19
+ viewActiveTab = 'import'
20
+
21
+ mount(siteName: string) {
22
+ this.switchTab('import', siteName)
23
+ }
24
+
25
+ switchTab(tab: string, siteName: string) {
26
+ if (tab === 'import') {
27
+ this.initImport()
28
+ } else if (tab === 'export') {
29
+ // if (window.confirm(`开始导出?`))
30
+ this.initExport()
31
+ return false
32
+ }
33
+ return true
34
+ }
35
+
36
+ initImport() {
37
+ this.$el.innerHTML =
38
+ `<div class="atk-log-wrap" style="display: none;">
39
+ <div class="atk-log-back-btn">返回</div>
40
+ <div class="atk-log"></div>
41
+ </div>
42
+ <div class="atk-form">
43
+ <div class="atk-label">数据类型</div>
44
+ <select name="AtkDataType">
45
+ <option value="artrans">Artrans (数据行囊)</option>
46
+ <option value="artalk_v1">Artalk v1 (PHP 旧版)</option>
47
+ <option value="typecho">Typecho</option>
48
+ <option value="wordpress">WordPress</option>
49
+ <option value="disqus">Disqus</option>
50
+ <option value="commento">Commento</option>
51
+ <option value="valine">Valine</option>
52
+ <option value="twikoo">Twikoo</option>
53
+ </select>
54
+ <div class="atk-label atk-data-file-label">数据文件</div>
55
+ <input type="file" name="AtkDataFile" accept="text/plain,.json">
56
+ <div class="atk-label">目标站点名</div>
57
+ <input type="text" name="AtkSiteName" placeholder="输入内容..." autocomplete="off">
58
+ <div class="atk-label">目标站点 URL</div>
59
+ <input type="text" name="AtkSiteURL" placeholder="输入内容..." autocomplete="off">
60
+ <div class="atk-label">启动参数(可选)</div>
61
+ <textarea name="AtkPayload"></textarea>
62
+ <span class="atk-desc">启动参数查阅:“<a href="https://artalk.js.org/guide/transfer.html" target="_blank">文档 · 数据搬家</a>”</span>
63
+ <button class="atk-btn" name="AtkSubmit">导入</button>
64
+ </div>`
65
+
66
+ const $form = this.$el.querySelector<HTMLSelectElement>('.atk-form')!
67
+ const $dataType = $form.querySelector<HTMLSelectElement>('[name="AtkDataType"]')!
68
+ const $dataFile = $form.querySelector<HTMLInputElement>('[name="AtkDataFile"]')!
69
+ const $dataFileLabel = $form.querySelector<HTMLInputElement>('.atk-data-file-label')!
70
+ const $siteName = $form.querySelector<HTMLInputElement>('[name="AtkSiteName"]')!
71
+ const $siteURL = $form.querySelector<HTMLInputElement>('[name="AtkSiteURL"]')!
72
+ const $payload = $form.querySelector<HTMLTextAreaElement>('[name="AtkPayload"]')!
73
+ const $submitBtn = $form.querySelector<HTMLButtonElement>('[name="AtkSubmit"]')!
74
+ const setError = (msg: string) => window.alert(msg)
75
+
76
+ $dataType.onchange = () => {
77
+ if (['typecho'].includes($dataType.value)) {
78
+ $dataFile.style.display = 'none'
79
+ $dataFileLabel.style.display = 'none'
80
+ } else {
81
+ $dataFile.style.display = ''
82
+ $dataFileLabel.style.display = ''
83
+ }
84
+ }
85
+
86
+ $submitBtn.onclick = () => {
87
+ const dataType = $dataType.value.trim()
88
+ const siteName = $siteName.value.trim()
89
+ const siteURL = $siteURL.value.trim()
90
+ const payload = $payload.value.trim()
91
+
92
+ if (dataType === '') {
93
+ setError('请选择数据类型')
94
+ return
95
+ }
96
+
97
+ // 请求 payload 参数制备
98
+ let rData: any = {}
99
+ if (payload) {
100
+ // JSON 格式检验
101
+ try {
102
+ rData = JSON.parse(payload)
103
+ } catch (err) {
104
+ setError(`Payload JSON 格式有误:${String(err)}`)
105
+ return
106
+ }
107
+
108
+ if (rData !instanceof Object) {
109
+ setError(`Payload 需为 JSON 对象`)
110
+ return
111
+ }
112
+ }
113
+ if (siteName) rData.t_name = siteName
114
+ if (siteURL) rData.t_url = siteURL
115
+
116
+ // 创建导入会话
117
+ const createSession = (dataStr?: string) => {
118
+ const $logWrap = this.$el.querySelector<HTMLElement>('.atk-log-wrap')!
119
+ const $log = $logWrap.querySelector<HTMLElement>('.atk-log')!
120
+ const $backBtn = this.$el.querySelector<HTMLElement>('.atk-log-back-btn')!
121
+
122
+ $logWrap.style.display = ''
123
+ $form.style.display = 'none'
124
+
125
+ $backBtn.onclick = () => {
126
+ $logWrap.style.display = 'none'
127
+ $form.style.display = ''
128
+ }
129
+
130
+ if (dataStr) rData.json_data = dataStr
131
+
132
+ // 创建 iframe
133
+ const frameName = `f_${+new Date()}`
134
+ const $frame = document.createElement('iframe')
135
+ $frame.className = 'atk-iframe'
136
+ $frame.name = frameName
137
+ $log.innerHTML = ''
138
+ $log.append($frame)
139
+
140
+ const formParams: {[k: string]: string} = {
141
+ type: dataType,
142
+ payload: JSON.stringify(rData),
143
+ token: this.ctx.user.data.token || '',
144
+ }
145
+
146
+ // 创建临时表单,初始化 iframe
147
+ const $formTmp = document.createElement('form')
148
+ $formTmp.style.display = 'none'
149
+ $formTmp.setAttribute('method', 'post')
150
+ $formTmp.setAttribute('action', `${this.ctx.conf.server}/admin/import`)
151
+ $formTmp.setAttribute("target", frameName)
152
+
153
+ Object.entries(formParams).forEach(([key, val]) => {
154
+ const $inputTmp = document.createElement('input')
155
+ $inputTmp.setAttribute('type', 'hidden')
156
+ $inputTmp.setAttribute('name', key)
157
+ $inputTmp.value = val
158
+ $formTmp.appendChild($inputTmp)
159
+ })
160
+
161
+ $logWrap.append($formTmp)
162
+ $formTmp.submit()
163
+ $formTmp.remove()
164
+ }
165
+
166
+ const reader = new FileReader()
167
+ reader.onload = () => {
168
+ const data = String(reader.result)
169
+ createSession(data)
170
+ }
171
+
172
+ // 是否已选择文件
173
+ if ($dataFile.files?.length) {
174
+ // 先读取文件
175
+ reader.readAsText($dataFile.files[0])
176
+ } else {
177
+ // 直接开始会话
178
+ createSession()
179
+ }
180
+ }
181
+ }
182
+
183
+ async initExport() {
184
+ Ui.showLoading(this.$el)
185
+
186
+ try {
187
+ const d = await new Api(this.ctx).export()
188
+ this.download(`artrans-${this.getYmdHisFilename()}.json`, d)
189
+ } catch (err: any) {
190
+ console.log(err)
191
+ window.alert(`${String(err)}`)
192
+ return
193
+ } finally { Ui.hideLoading(this.$el) }
194
+ }
195
+
196
+ download(filename: string, text: string) {
197
+ const el = document.createElement('a')
198
+ el.setAttribute('href', `data:text/json;charset=utf-8,${encodeURIComponent(text)}`);
199
+ el.setAttribute('download', filename)
200
+ el.style.display = 'none'
201
+ document.body.appendChild(el)
202
+ el.click()
203
+ document.body.removeChild(el)
204
+ }
205
+
206
+ getYmdHisFilename() {
207
+ const date = new Date()
208
+
209
+ const year = date.getFullYear()
210
+ const month = date.getMonth() + 1
211
+ const day = date.getDate()
212
+ const hours = date.getHours()
213
+ const minutes = date.getMinutes()
214
+ const seconds = date.getSeconds()
215
+
216
+ return `${year}${month}${day}-${hours}${Utils.padWithZeros(minutes, 2)}${Utils.padWithZeros(seconds, 2)}`
217
+ }
218
+ }
@@ -0,0 +1,288 @@
1
+ import '../style/sidebar.less'
2
+
3
+ import MD5 from '../lib/md5'
4
+ import Context from '../context'
5
+ import Component from '../lib/component'
6
+ import * as Utils from '../lib/utils'
7
+ import * as Ui from '../lib/ui'
8
+ import Comment from './comment'
9
+ import SidebarHTML from './html/sidebar.html?raw'
10
+ import Layer from './layer'
11
+
12
+ import SidebarView from './sidebar-view'
13
+ import CommentsView from './sidebar-views/comments-view'
14
+ import PagesView from './sidebar-views/pages-view'
15
+ import SitesView from './sidebar-views/sites-view'
16
+ import TransferView from './sidebar-views/transfer-view'
17
+ import { SiteData } from '~/types/artalk-data'
18
+ import Api from '../api'
19
+ import SiteListFloater from './admin/site-list-floater'
20
+ import SettingView from './sidebar-views/setting-view'
21
+
22
+ const DEFAULT_VIEW = 'comments'
23
+ const REGISTER_VIEWS: (typeof SidebarView)[] = [
24
+ CommentsView, PagesView, SitesView, TransferView, // SettingView
25
+ ]
26
+
27
+ export default class Sidebar extends Component {
28
+ public layer?: Layer
29
+
30
+ public $header: HTMLElement
31
+ public $headerMenu: HTMLElement
32
+ public $title: HTMLElement
33
+ public $avatar: HTMLElement
34
+ public $siteLogo?: HTMLElement
35
+ public $closeBtn: HTMLElement
36
+ public $nav: HTMLElement
37
+ public $curtViewBtn: HTMLElement
38
+ public $curtViewBtnIcon: HTMLElement
39
+ public $curtViewBtnText: HTMLElement
40
+ public $navTabs: HTMLElement
41
+ public $navViews: HTMLElement
42
+ public $viewWrap: HTMLElement
43
+ public siteSwitcher?: SiteListFloater
44
+
45
+ private get isAdmin() { return this.ctx.user.data.isAdmin }
46
+ public curtSite?: string
47
+ public curtView: string = DEFAULT_VIEW
48
+ public get curtViewInstance() {
49
+ return this.curtView ? this.viewInstances[this.curtView] : undefined
50
+ }
51
+ public curtTab?: string
52
+
53
+ public viewInstances: {[name: string]: SidebarView} = {}
54
+
55
+ private viewSwitcherShow = false
56
+
57
+ constructor(ctx: Context) {
58
+ super(ctx)
59
+
60
+ // initial elements
61
+ this.$el = Utils.createElement(SidebarHTML)
62
+ this.$header = this.$el.querySelector('.atk-sidebar-header')!
63
+ this.$headerMenu = this.$header.querySelector('.atk-menu')!
64
+ this.$title = this.$header.querySelector('.atk-sidebar-title')!
65
+ this.$avatar = this.$header.querySelector('.atk-avatar')!
66
+ this.$closeBtn = this.$header.querySelector('.atk-sidebar-close')!
67
+
68
+ this.$nav = this.$el.querySelector('.atk-sidebar-nav')!
69
+ this.$curtViewBtn = this.$nav.querySelector('.akt-curt-view-btn')!
70
+ this.$curtViewBtnIcon = this.$curtViewBtn.querySelector('.atk-icon')!
71
+ this.$curtViewBtnText = this.$curtViewBtn.querySelector('.atk-text')!
72
+ this.$navTabs = this.$nav.querySelector('.atk-tabs')!
73
+ this.$navViews = this.$nav.querySelector('.atk-views')!
74
+
75
+ this.$viewWrap = this.$el.querySelector('.atk-sidebar-view-wrap')!
76
+
77
+ // init UI
78
+ this.initViewSwitcher()
79
+
80
+ this.$closeBtn.onclick = () => {
81
+ this.hide()
82
+ }
83
+
84
+ // event
85
+ this.ctx.on('sidebar-show', () => (this.show()))
86
+ this.ctx.on('sidebar-hide', () => (this.hide()))
87
+ this.ctx.on('user-changed', () => { this.firstShow = true })
88
+ }
89
+
90
+ /** 初始化 view 切换器 */
91
+ private initViewSwitcher() {
92
+ this.$curtViewBtn.onclick = () => {
93
+ this.toggleViewSwitcher()
94
+ }
95
+
96
+ this.$navViews.innerHTML = ''
97
+ REGISTER_VIEWS.forEach(view => {
98
+ const $item = Utils.createElement(`<div class="atk-tab-item"></div>`)
99
+ this.$navViews.append($item)
100
+ $item.setAttribute('data-name', view.viewName)
101
+ $item.innerText = view.viewTitle
102
+ if (view.viewName === this.curtView) {
103
+ $item.classList.add('atk-active')
104
+ this.$curtViewBtnText.innerText = view.viewTitle
105
+ }
106
+ $item.onclick = () => {
107
+ // 切换 view
108
+ this.switchView(view.viewName)
109
+
110
+ this.toggleViewSwitcher()
111
+ }
112
+ })
113
+ }
114
+
115
+ /** 显示/隐藏 view 切换器 */
116
+ private toggleViewSwitcher() {
117
+ if (!this.viewSwitcherShow) {
118
+ // 显示
119
+ this.$navViews.style.display = ''
120
+ this.$navTabs.style.display = 'none'
121
+ this.$curtViewBtnIcon.classList.add('atk-arrow')
122
+ // this.$curtViewBtnText.style.display = 'none'
123
+ } else {
124
+ // 隐藏
125
+ this.$navViews.style.display = 'none'
126
+ this.$navTabs.style.display = ''
127
+ this.$curtViewBtnIcon.classList.remove('atk-arrow')
128
+ // this.$curtViewBtnText.style.display = ''
129
+ }
130
+
131
+ this.viewSwitcherShow = !this.viewSwitcherShow
132
+ }
133
+
134
+ private firstShow = true
135
+
136
+ /** 显示 */
137
+ public async show() {
138
+ this.$el.style.transform = '' // 动画清除,防止二次打开失效
139
+
140
+ // 获取 Layer
141
+ this.layer = new Layer(this.ctx, 'sidebar', this.$el)
142
+ this.layer.show()
143
+
144
+ // viewWrap 滚动条归位
145
+ this.$viewWrap.scrollTo(0, 0)
146
+
147
+ // 执行动画
148
+ setTimeout(() => {
149
+ this.$el.style.transform = 'translate(0, 0)'
150
+ }, 20)
151
+
152
+ // 第一次加载
153
+ if (this.firstShow) {
154
+ ///////////////////
155
+ //// IMPORTANT ////
156
+ ///////////////////
157
+
158
+ // 用户权限检测
159
+ if (this.isAdmin) {
160
+ // 是管理员
161
+ this.$title.innerText = '控制中心'
162
+ this.$curtViewBtn.style.display = ''
163
+
164
+ if (!this.siteSwitcher) {
165
+ // 初始化站点切换器
166
+ this.siteSwitcher = new SiteListFloater(this.ctx, {
167
+ onSwitchSite: (siteName) => { this.switchSite(siteName) },
168
+ onClickSitesViewBtn: () => { this.switchView('sites') }
169
+ })
170
+ this.$viewWrap.before(this.siteSwitcher.$el)
171
+ this.$avatar.onclick = (evt) => {
172
+ if (!this.isAdmin) return
173
+ this.siteSwitcher?.show(evt.target as any)
174
+ }
175
+
176
+ }
177
+
178
+ this.curtSite = this.conf.site
179
+
180
+ Ui.showLoading(this.$el)
181
+ try {
182
+ await this.siteSwitcher!.load(this.curtSite)
183
+ } catch (err: any) {
184
+ const $err = Utils.createElement(`<span>加载失败:${err.msg || '网络错误'}<br/></span>`)
185
+ const $retryBtn = Utils.createElement('<span style="cursor:pointer;">点击重新获取</span>')
186
+ $err.appendChild($retryBtn)
187
+ $retryBtn.onclick = () => { Ui.setError(this.$el, null); this.show() }
188
+ Ui.setError(this.$el, $err)
189
+ return
190
+ } finally { Ui.hideLoading(this.$el) }
191
+
192
+ // 站点图像
193
+ this.$avatar.innerHTML = ''
194
+ this.$siteLogo = Utils.createElement('<div class="atk-site-logo"></div>')
195
+ this.$siteLogo.innerText = (this.curtSite || '').substr(0, 1)
196
+ this.$avatar.append(this.$siteLogo)
197
+ } else {
198
+ // 不是管理员
199
+ this.$title.innerText = '通知中心'
200
+ this.$curtViewBtn.style.display = 'none' // 隐藏 view 切换器
201
+ this.curtSite = this.conf.site // 第一次 show 使用当前站点数据
202
+
203
+ // 头像
204
+ const $avatarImg = document.createElement('img') as HTMLImageElement
205
+ $avatarImg.src = Utils.getGravatarURL(this.ctx, MD5(this.ctx.user.data.email.toLowerCase()))
206
+ this.$avatar.innerHTML = ''
207
+ this.$avatar.append($avatarImg)
208
+ }
209
+
210
+ this.switchView(DEFAULT_VIEW) // 打开默认 view
211
+ this.firstShow = false
212
+ }
213
+ }
214
+
215
+ /** 隐藏 */
216
+ public hide() {
217
+ // 执行动画
218
+ this.$el.style.transform = ''
219
+
220
+ // 用完即销毁
221
+ this.layer?.dispose()
222
+ }
223
+
224
+ /** 切换 View */
225
+ public switchView(viewName: string) {
226
+ let view = this.viewInstances[viewName]
227
+ if (!view) {
228
+ // 初始化 View
229
+ const View = REGISTER_VIEWS.find(o => o.viewName === viewName)!
230
+ view = new View(this.ctx, this.$viewWrap)
231
+ this.viewInstances[viewName] = view
232
+ }
233
+
234
+ // init view
235
+ view.mount(this.curtSite!)
236
+
237
+ this.curtView = viewName
238
+ this.curtTab = view.viewActiveTab
239
+
240
+ // update view indicator
241
+ this.$curtViewBtnText.innerText = (view.constructor as typeof SidebarView).viewTitle
242
+ this.$navViews.querySelectorAll('.atk-tab-item').forEach((e) => {
243
+ if (e.getAttribute('data-name') === viewName) {
244
+ e.classList.add('atk-active')
245
+ } else {
246
+ e.classList.remove('atk-active')
247
+ }
248
+ })
249
+
250
+ // update tabs
251
+ this.loadViewTabs(view)
252
+
253
+ // load element
254
+ this.$viewWrap.innerHTML = ''
255
+ this.$viewWrap.append(view.$el)
256
+ this.$viewWrap.classList.forEach((c) => {
257
+ if (c.startsWith('atk-view-name-'))
258
+ this.$viewWrap.classList.remove(c)
259
+ })
260
+ this.$viewWrap.classList.add(`atk-view-name-${(view.constructor as typeof SidebarView).viewName}`)
261
+ }
262
+
263
+ private loadViewTabs(view: SidebarView) {
264
+ this.$navTabs.innerHTML = ''
265
+ Object.entries<string>(view.viewTabs).forEach(([tabName, label]) => {
266
+ const $tab = Utils.createElement(`<div class="atk-tab-item"></div>`)
267
+ this.$navTabs.append($tab)
268
+ $tab.innerText = label
269
+ if (view.viewActiveTab === tabName) $tab.classList.add('atk-active')
270
+
271
+ // 切换 tab
272
+ $tab.onclick = () => {
273
+ if (view.switchTab(tabName, this.curtSite!) === false) { return }
274
+ this.$navTabs.querySelectorAll('.atk-active').forEach(e => e.classList.remove('atk-active'))
275
+ $tab.classList.add('atk-active')
276
+ this.curtTab = tabName
277
+ }
278
+ })
279
+ }
280
+
281
+ /** 切换站点 */
282
+ private switchSite(siteName: string) {
283
+ this.curtSite = siteName
284
+ const curtView = this.curtViewInstance
285
+ curtView?.switchTab(this.curtTab!, siteName)
286
+ if (this.$siteLogo) this.$siteLogo.innerText = this.curtSite.substr(0, 1)
287
+ }
288
+ }
package/src/context.ts ADDED
@@ -0,0 +1,42 @@
1
+ import User from './lib/user'
2
+ import ArtalkConfig from '~/types/artalk-config'
3
+ import { EventPayloadMap, Event, EventScopeType, Handler } from '~/types/event'
4
+
5
+ /**
6
+ * Artalk Context
7
+ */
8
+ export default class Context {
9
+ public cid: number // Context 唯一标识
10
+ public $root: HTMLElement
11
+ public conf: ArtalkConfig
12
+ public user: User
13
+
14
+ private eventList: Event[] = []
15
+
16
+ public constructor (rootEl: HTMLElement, conf: ArtalkConfig) {
17
+ this.cid = +new Date()
18
+ this.$root = rootEl
19
+ this.conf = conf
20
+ this.user = new User(this.conf)
21
+
22
+ this.$root.setAttribute('atk-run-id', this.cid.toString())
23
+ }
24
+
25
+ public on<K extends keyof EventPayloadMap>(name: K, handler: Handler<EventPayloadMap[K]>, scope: EventScopeType = 'internal') {
26
+ this.eventList.push({ name, handler: handler as any, scope })
27
+ }
28
+
29
+ public off<K extends keyof EventPayloadMap>(name: K, handler?: Handler<EventPayloadMap[K]>, scope: EventScopeType = 'internal') {
30
+ this.eventList = this.eventList.filter((evt) => {
31
+ if (handler) return !(evt.name === name && evt.handler === handler && evt.scope === scope)
32
+ return !(evt.name === name && evt.scope === scope) // 删除全部相同 name event
33
+ })
34
+ }
35
+
36
+ public trigger<K extends keyof EventPayloadMap>(name: K, payload?: EventPayloadMap[K], scope?: EventScopeType) {
37
+ this.eventList
38
+ .filter((evt) => evt.name === name && (scope ? (evt.scope === scope) : true))
39
+ .map((evt) => evt.handler)
40
+ .forEach((handler) => handler(payload as any))
41
+ }
42
+ }
@@ -0,0 +1,42 @@
1
+ import ArtalkConfig from "~/types/artalk-config";
2
+ import emoticons from './assets/emoticons.json'
3
+
4
+ const defaultConf: ArtalkConfig = {
5
+ el: '',
6
+ pageKey: '',
7
+ server: '',
8
+ site: '',
9
+
10
+ placeholder: '键入内容...',
11
+ noComment: '「此时无声胜有声」',
12
+ sendBtn: '发送评论',
13
+ darkMode: false,
14
+
15
+ emoticons,
16
+
17
+ vote: true,
18
+ voteDown: false,
19
+ uaBadge: true,
20
+ flatMode: 'auto',
21
+ maxNesting: 3,
22
+ gravatar: {
23
+ default: 'mp',
24
+ mirror: 'https://sdn.geekzu.org/avatar/',
25
+ },
26
+
27
+ pagination: {
28
+ pageSize: 15,
29
+ readMore: true,
30
+ autoLoad: true,
31
+ },
32
+
33
+ heightLimit: {
34
+ content: 200,
35
+ children: 300,
36
+ },
37
+
38
+ reqTimeout: 15000,
39
+ versionCheck: true,
40
+ }
41
+
42
+ export default defaultConf
@@ -0,0 +1,33 @@
1
+ import Api from '@/api'
2
+ import * as Utils from '../utils'
3
+ import { Checker } from '.'
4
+
5
+ const AdminChecker: Checker = {
6
+ inputType: 'password',
7
+
8
+ request(that, inputVal) {
9
+ const data = {
10
+ name: that.ctx.user.data.nick,
11
+ email: that.ctx.user.data.email,
12
+ password: inputVal
13
+ }
14
+
15
+ return new Api(that.ctx).login(data.name, data.email, data.password)
16
+ },
17
+
18
+ body() {
19
+ return Utils.createElement('<span>敲入密码来验证管理员身份:</span>')
20
+ },
21
+
22
+ onSuccess(that, userToken, inputVal, formEl) {
23
+ that.ctx.user.data.isAdmin = true
24
+ that.ctx.user.data.token = userToken
25
+ that.ctx.user.save()
26
+ that.ctx.trigger('user-changed', that.ctx.user.data)
27
+ that.ctx.trigger('list-reload')
28
+ },
29
+
30
+ onError(that, err, inputVal, formEl) {}
31
+ }
32
+
33
+ export default AdminChecker
@@ -0,0 +1,38 @@
1
+ import Api from '@/api'
2
+ import * as Utils from '../utils'
3
+ import { Checker } from '.'
4
+
5
+ const CaptchaChecker: Checker = {
6
+ request(that, inputVal) {
7
+ return new Api(that.ctx).captchaCheck(inputVal)
8
+ },
9
+
10
+ body(that) {
11
+ const elem = Utils.createElement(
12
+ `<span><img class="atk-captcha-img" src="${that.submitCaptchaImgData || ''}" alt="验证码">敲入验证码继续:</span>`
13
+ );
14
+
15
+ // 刷新验证码
16
+ elem.querySelector<HTMLElement>('.atk-captcha-img')!.onclick = () => {
17
+ const imgEl = elem.querySelector('.atk-captcha-img')
18
+ new Api(that.ctx).captchaGet()
19
+ .then((imgData) => {
20
+ imgEl!.setAttribute('src', imgData)
21
+ })
22
+ .catch((err) => {
23
+ console.error('验证码获取失败 ', err)
24
+ })
25
+ }
26
+ return elem
27
+ },
28
+
29
+ onSuccess(that, data, inputVal, formEl) {
30
+ that.submitCaptchaVal = inputVal
31
+ },
32
+
33
+ onError(that, err, inputVal, formEl) {
34
+ formEl.querySelector<HTMLElement>('.atk-captcha-img')!.click() // 刷新验证码
35
+ }
36
+ }
37
+
38
+ export default CaptchaChecker