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.
- package/README.md +7 -60
- package/dist/Artalk.css +1 -1
- package/dist/Artalk.es.js +5996 -1
- package/dist/Artalk.iife.js +266 -1
- package/dist/Artalk.js +266 -1
- package/package.json +3 -4
- package/src/api/index.ts +269 -0
- package/src/api/request.ts +125 -0
- package/src/artalk.ts +153 -0
- package/src/components/_component.ts.txt +10 -0
- package/src/components/action-btn.ts +169 -0
- package/src/components/admin/page-list.ts +194 -0
- package/src/components/admin/site-list-floater.ts +98 -0
- package/src/components/admin/site-list.ts +289 -0
- package/src/components/comment.ts +478 -0
- package/src/components/dialog.ts +63 -0
- package/src/components/editor-plugs/{_ExamplePlug.ts.txt → _example-plug.ts.txt} +0 -0
- package/src/components/editor-plugs/{EditorPlug.ts → editor-plug.ts} +3 -3
- package/src/components/editor-plugs/{EmoticonsPlug.less → emoticons-plug.less} +0 -0
- package/src/components/editor-plugs/{EmoticonsPlug.ts → emoticons-plug.ts} +9 -9
- package/src/components/editor-plugs/{PreviewPlug.less → preview-plug.less} +0 -0
- package/src/components/editor-plugs/{PreviewPlug.ts → preview-plug.ts} +10 -10
- package/src/components/{Editor.ts → editor.ts} +104 -102
- package/src/components/html/comment.html +2 -6
- package/src/components/html/list.html +0 -1
- package/src/components/html/sidebar.html +18 -4
- package/src/components/item-text-editor.ts +136 -0
- package/src/components/layer.ts +135 -0
- package/src/components/list-lite.ts +507 -0
- package/src/components/list.ts +172 -0
- package/src/components/pagination.ts +150 -0
- package/src/components/read-more-btn.ts +71 -0
- package/src/components/sidebar-view.ts +26 -0
- package/src/components/sidebar-views/comments-view.ts +66 -0
- package/src/components/sidebar-views/pages-view.ts +61 -0
- package/src/components/sidebar-views/setting-view.ts +21 -0
- package/src/components/sidebar-views/sites-view.ts +42 -0
- package/src/components/sidebar-views/transfer-view.ts +218 -0
- package/src/components/sidebar.ts +288 -0
- package/src/context.ts +42 -0
- package/src/default-conf.ts +42 -0
- package/src/lib/checker/admin-checker.ts +33 -0
- package/src/lib/checker/captcha-checker.ts +38 -0
- package/src/lib/checker/index.ts +134 -0
- package/src/lib/component.ts +3 -3
- package/src/lib/md5.js +6 -0
- package/src/lib/ui.ts +64 -74
- package/src/lib/user.ts +2 -8
- package/src/lib/utils.ts +30 -11
- package/src/main.ts +1 -1
- package/src/style/_color.less +8 -2
- package/src/style/_extend.less +81 -0
- package/src/style/_variables.less +3 -0
- package/src/style/comment.less +60 -24
- package/src/style/editor.less +3 -0
- package/src/style/item-text-editor.less +35 -0
- package/src/style/list.less +51 -32
- package/src/style/main.less +75 -1
- package/src/style/page-list.less +50 -0
- package/src/style/pagination.less +58 -0
- package/src/style/sidebar.less +274 -199
- package/src/style/site-list.less +208 -0
- package/tsconfig.json +27 -0
- package/types/artalk-config.d.ts +68 -51
- package/types/artalk-data.d.ts +38 -1
- package/types/event.d.ts +73 -0
- package/types/index.d.ts +1 -6
- package/.github/workflows/npm.yml +0 -31
- package/.github/workflows/pages.yml +0 -30
- package/dist/src/Event.d.ts +0 -59
- package/dist/types/artalk-config.d.ts +0 -79
- package/dist/types/artalk-data.d.ts +0 -153
- package/dist/types/global.d.ts +0 -1
- package/dist/types/index.d.ts +0 -8
- package/dist/types/vite-env.d.ts +0 -1
- package/example/pjax/res1.html +0 -41
- package/example/pjax/res2.html +0 -41
- package/src/Artalk.ts +0 -160
- package/src/Constant.ts +0 -6
- package/src/Context.ts +0 -34
- package/src/Event.d.ts +0 -59
- package/src/components/Comment.ts +0 -402
- package/src/components/Layer.ts +0 -138
- package/src/components/List.ts +0 -162
- package/src/components/ListLite.ts +0 -407
- package/src/components/Sidebar.ts +0 -141
- package/src/components/sidebar-views/AdminView.ts +0 -523
- package/src/components/sidebar-views/MessageView.ts +0 -48
- package/src/components/sidebar-views/SidebarView.ts +0 -24
- package/src/lib/api.ts +0 -380
- 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
|