artalk 2.1.4 → 2.1.7
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/CNAME +1 -0
- package/dist/Artalk.css +1 -1
- package/dist/Artalk.es.js +303 -1248
- package/dist/Artalk.iife.js +1 -1
- package/dist/Artalk.js +1 -1
- package/index.html +143 -0
- package/package.json +8 -29
- package/postcss.config.js +5 -0
- package/src/api/index.ts +14 -0
- package/src/api/request.ts +4 -2
- package/src/artalk.ts +28 -6
- package/src/components/comment.ts +20 -3
- package/src/components/html/list.html +4 -2
- package/src/components/html/sidebar-layer.html +8 -0
- package/src/components/list.ts +60 -0
- package/src/components/sidebar-layer.ts +143 -0
- package/src/defaults.ts +1 -0
- package/src/lib/detect.js +3 -1
- package/src/lib/utils.ts +25 -0
- package/src/style/_extend.less +0 -80
- package/src/style/comment.less +6 -0
- package/src/style/list.less +66 -0
- package/src/style/main.less +1 -1
- package/src/style/sidebar-layer.less +92 -0
- package/types/artalk-config.d.ts +4 -1
- package/types/artalk-data.d.ts +3 -0
- package/vite-common.config.ts +15 -0
- package/vite-demo.config.ts +14 -0
- package/vite.config.ts +36 -0
- package/LICENSE +0 -674
- package/README.md +0 -84
- package/src/components/admin/page-list.ts +0 -194
- package/src/components/admin/site-list-floater.ts +0 -98
- package/src/components/admin/site-list.ts +0 -289
- package/src/components/html/sidebar.html +0 -22
- package/src/components/item-text-editor.ts +0 -136
- package/src/components/sidebar-view.ts +0 -26
- package/src/components/sidebar-views/comments-view.ts +0 -73
- package/src/components/sidebar-views/pages-view.ts +0 -61
- package/src/components/sidebar-views/setting-view.ts +0 -21
- package/src/components/sidebar-views/sites-view.ts +0 -36
- package/src/components/sidebar-views/transfer-view.ts +0 -218
- package/src/components/sidebar.ts +0 -293
- package/src/lib/md5.js +0 -6
- package/src/style/item-text-editor.less +0 -35
- package/src/style/page-list.less +0 -50
- package/src/style/sidebar.less +0 -420
- package/src/style/site-list.less +0 -208
package/README.md
DELETED
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
<p align="center">
|
|
2
|
-
<img src="https://user-images.githubusercontent.com/22412567/137740516-d9e97af0-fb3b-4dab-b331-671a9a2a3a63.png" alt="Artalk" width="100%">
|
|
3
|
-
</p>
|
|
4
|
-
|
|
5
|
-
# Artalk
|
|
6
|
-
|
|
7
|
-
[](https://www.npmjs.com/package/artalk)
|
|
8
|
-
[](https://www.npmjs.com/package/artalk)
|
|
9
|
-
[](https://github.com/ArtalkJS/Artalk/commits/master)
|
|
10
|
-
[](https://github.com/ArtalkJS/Artalk/issues)
|
|
11
|
-
[](https://github.com/ArtalkJS/Artalk/pulls)
|
|
12
|
-
[](https://travis-ci.com/ArtalkJS/Artalk)
|
|
13
|
-
[](./LICENSE)
|
|
14
|
-
|
|
15
|
-
> 🌌 一款简洁的自托管评论系统 | A Selfhosted Comment System.
|
|
16
|
-
|
|
17
|
-
前往:[“**官方文档**”](https://artalk.js.org)
|
|
18
|
-
|
|
19
|
-
---
|
|
20
|
-
|
|
21
|
-
- 轻量 (~30kB gzipped)
|
|
22
|
-
- 自托管 (隐私安全)
|
|
23
|
-
- 简单易上手 (防秃顶)
|
|
24
|
-
- Markdown 语法 (通用性)
|
|
25
|
-
- Golang 后端 (易部署 / 跨平台)
|
|
26
|
-
|
|
27
|
-
## 特性
|
|
28
|
-
|
|
29
|
-
- 表 情 包 :统一管理 / 链接替换
|
|
30
|
-
- 通知中心:红点标记 / 已读记录
|
|
31
|
-
- 身份验证:徽标自定义 / 密码验证
|
|
32
|
-
- 站点隔离:多站点管理 / 站点管理员用户分配
|
|
33
|
-
- 评论审核:反垃圾检测 / 验证码 / 提交频率限制
|
|
34
|
-
- 邮件提醒:模版自定义 / 多管理员通知
|
|
35
|
-
- 树洞模式:仅自己可见 / 说说功能
|
|
36
|
-
- 页面管理:标题可显示 / 快速跳转
|
|
37
|
-
- 无限层级:可切换为 “平铺模式”
|
|
38
|
-
- 评论投票:赞同还是反对评论
|
|
39
|
-
- 异步处理:邮件发送无需等待
|
|
40
|
-
- 滚动加载:评论内容分页处置
|
|
41
|
-
- 自动保存:用户输入防丢功能
|
|
42
|
-
- 自动填充:用户链接自动填充
|
|
43
|
-
- 暗黑模式:防止眼部疾病伤害
|
|
44
|
-
- 评论折叠:这个不打算给你康
|
|
45
|
-
- 数据备份:防止评论数据丢失
|
|
46
|
-
- 数据迁移:在不同评论系统之间来回切换
|
|
47
|
-
- 一页多评:一页多个评论区(似乎没啥用
|
|
48
|
-
- TypeScript & Vanilla:纯天然无添加 / 无需依赖
|
|
49
|
-
- [Vite](https://github.com/vitejs/vite):开发者的极致体验
|
|
50
|
-
|
|
51
|
-
## 食用方针
|
|
52
|
-
|
|
53
|
-
前往:[“**文档 · 部署**”](https://artalk.js.org/guide/deploy.html)
|
|
54
|
-
|
|
55
|
-
```sh
|
|
56
|
-
$ yarn add artalk
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
```ts
|
|
60
|
-
import Artalk from 'artalk'
|
|
61
|
-
|
|
62
|
-
new Artalk({
|
|
63
|
-
el: '#Comments',
|
|
64
|
-
pageKey: '<页面链接>',
|
|
65
|
-
pageTitle: '<页面标题>',
|
|
66
|
-
server: '<后端地址>',
|
|
67
|
-
site: '<站点名称>',
|
|
68
|
-
})
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
## 特别致谢
|
|
72
|
-
|
|
73
|
-
感谢社区提供的帮助与反馈,若有好的建议与意见欢迎前往 [ISSUES](https://github.com/ArtalkJS/Artalk/issues) 随时告知。
|
|
74
|
-
|
|
75
|
-
## Stargazers over time
|
|
76
|
-
|
|
77
|
-
[](https://starchart.cc/ArtalkJS/Artalk)
|
|
78
|
-
|
|
79
|
-
## License
|
|
80
|
-
[GPL-3.0](./LICENSE)
|
|
81
|
-
|
|
82
|
-
[](https://app.fossa.com/projects/git%2Bgithub.com%2FArtalkJS%2FArtalk?ref=badge_shield)
|
|
83
|
-
|
|
84
|
-
[](https://app.fossa.com/projects/git%2Bgithub.com%2FArtalkJS%2FArtalk?ref=badge_large)
|
|
@@ -1,194 +0,0 @@
|
|
|
1
|
-
import '@/style/page-list.less'
|
|
2
|
-
|
|
3
|
-
import Context from '@/context'
|
|
4
|
-
import Component from '@/lib/component'
|
|
5
|
-
import * as Utils from '@/lib/utils'
|
|
6
|
-
import * as Ui from '@/lib/ui'
|
|
7
|
-
import { PageData } from '~/types/artalk-data'
|
|
8
|
-
import ItemTextEditor from '../item-text-editor'
|
|
9
|
-
import Api from '~/src/api'
|
|
10
|
-
import ActionBtn from '../action-btn'
|
|
11
|
-
|
|
12
|
-
export default class PageList extends Component {
|
|
13
|
-
$editor?: HTMLElement
|
|
14
|
-
$inputer?: HTMLElement
|
|
15
|
-
|
|
16
|
-
pages: PageData[] = []
|
|
17
|
-
|
|
18
|
-
constructor(ctx: Context) {
|
|
19
|
-
super(ctx)
|
|
20
|
-
|
|
21
|
-
this.$el = Utils.createElement(`<div class="atk-page-list"></div>`)
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/** 清空所有 Pages */
|
|
25
|
-
clearAll() {
|
|
26
|
-
this.pages = []
|
|
27
|
-
this.$el.innerHTML = ''
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/** 导入 Page 数据 */
|
|
31
|
-
importPages(pages: PageData[]) {
|
|
32
|
-
this.pages.push(...pages)
|
|
33
|
-
|
|
34
|
-
pages.forEach((page) => {
|
|
35
|
-
const $page = this.renderPage(page)
|
|
36
|
-
this.$el.append($page)
|
|
37
|
-
})
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/** Page Element */
|
|
41
|
-
renderPage(page: PageData) {
|
|
42
|
-
const $page = Utils.createElement(
|
|
43
|
-
`<div class="atk-page-item">
|
|
44
|
-
<div class="atk-page-main">
|
|
45
|
-
<div class="atk-title"></div>
|
|
46
|
-
<div class="atk-sub"></div>
|
|
47
|
-
</div>
|
|
48
|
-
<div class="atk-page-actions">
|
|
49
|
-
<div class="atk-item atk-edit-btn">
|
|
50
|
-
<i class="atk-icon atk-icon-edit"></i>
|
|
51
|
-
</div>
|
|
52
|
-
</div>
|
|
53
|
-
</div>`)
|
|
54
|
-
|
|
55
|
-
const $main = $page.querySelector<HTMLElement>('.atk-page-main')!
|
|
56
|
-
const $title = $main.querySelector<HTMLElement>('.atk-title')!
|
|
57
|
-
const $sub = $main.querySelector<HTMLElement>('.atk-sub')!
|
|
58
|
-
const $editBtn = $page.querySelector<HTMLElement>('.atk-edit-btn')!
|
|
59
|
-
|
|
60
|
-
$title.innerText = page.title
|
|
61
|
-
$sub.innerText = page.url || page.key
|
|
62
|
-
$editBtn.onclick = () => this.showEditor(page, $page)
|
|
63
|
-
|
|
64
|
-
return $page
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/** 显示编辑器 */
|
|
68
|
-
showEditor(page: PageData, $page: HTMLElement) {
|
|
69
|
-
this.closeEditor()
|
|
70
|
-
|
|
71
|
-
this.$editor = Utils.createElement(
|
|
72
|
-
`<div class="atk-page-edit-layer">
|
|
73
|
-
<div class="atk-page-main-actions">
|
|
74
|
-
<div class="atk-item atk-title-edit-btn">标题修改</div>
|
|
75
|
-
<div class="atk-item atk-key-edit-btn">KEY 变更</div>
|
|
76
|
-
<div class="atk-item atk-admin-only-btn"></div>
|
|
77
|
-
</div>
|
|
78
|
-
<div class="atk-page-actions">
|
|
79
|
-
<div class="atk-item atk-sync-btn">
|
|
80
|
-
<i class="atk-icon atk-icon-sync"></i>
|
|
81
|
-
</div>
|
|
82
|
-
<div class="atk-item atk-del-btn">
|
|
83
|
-
<i class="atk-icon atk-icon-del"></i>
|
|
84
|
-
</div>
|
|
85
|
-
<div class="atk-item atk-close-btn">
|
|
86
|
-
<i class="atk-icon atk-icon-close"></i>
|
|
87
|
-
</div>
|
|
88
|
-
</div>
|
|
89
|
-
</div>`)
|
|
90
|
-
$page.prepend(this.$editor)
|
|
91
|
-
|
|
92
|
-
const $titleEditBtn = this.$editor.querySelector<HTMLElement>('.atk-title-edit-btn')!
|
|
93
|
-
const $keyEditBtn = this.$editor.querySelector<HTMLElement>('.atk-key-edit-btn')!
|
|
94
|
-
const $adminOnlyBtn = this.$editor.querySelector<HTMLElement>('.atk-admin-only-btn')!
|
|
95
|
-
const $syncBtn = this.$editor.querySelector<HTMLElement>('.atk-sync-btn')!
|
|
96
|
-
const $delBtn = this.$editor.querySelector<HTMLElement>('.atk-del-btn')!
|
|
97
|
-
const $closeBtn = this.$editor.querySelector<HTMLElement>('.atk-close-btn')!
|
|
98
|
-
const showLoading = () => { Ui.showLoading(this.$editor!) }
|
|
99
|
-
const hideLoading = () => { Ui.hideLoading(this.$editor!) }
|
|
100
|
-
const showError = (msg: string) => { window.alert(msg) }
|
|
101
|
-
|
|
102
|
-
// 关闭编辑器
|
|
103
|
-
$closeBtn.onclick = () => this.closeEditor()
|
|
104
|
-
|
|
105
|
-
// 文本编辑
|
|
106
|
-
const openTextEditor = (key: string) => {
|
|
107
|
-
const textEditor = new ItemTextEditor({
|
|
108
|
-
initValue: page[key] || '',
|
|
109
|
-
onYes: async (val: string) => {
|
|
110
|
-
Ui.showLoading(textEditor.$el)
|
|
111
|
-
let p: PageData
|
|
112
|
-
try {
|
|
113
|
-
p = await new Api(this.ctx).pageEdit({ ...page, [key]: val })
|
|
114
|
-
} catch (err: any) {
|
|
115
|
-
showError(`修改失败:${err.msg || '未知错误'}`)
|
|
116
|
-
console.error(err)
|
|
117
|
-
return false
|
|
118
|
-
} finally { Ui.hideLoading(textEditor.$el) }
|
|
119
|
-
$page.replaceWith(this.renderPage(p))
|
|
120
|
-
return true
|
|
121
|
-
}
|
|
122
|
-
})
|
|
123
|
-
textEditor.appendTo(this.$editor!)
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// 标题修改
|
|
127
|
-
$titleEditBtn.onclick = () => openTextEditor('title')
|
|
128
|
-
|
|
129
|
-
// Key 修改
|
|
130
|
-
$keyEditBtn.onclick = () => openTextEditor('key')
|
|
131
|
-
|
|
132
|
-
// 仅管理员可评
|
|
133
|
-
const adminOnlyActionBtn = new ActionBtn({
|
|
134
|
-
text: () => {
|
|
135
|
-
$adminOnlyBtn.classList.remove('atk-green', 'atk-yellow')
|
|
136
|
-
$adminOnlyBtn.classList.add(!page.admin_only ? 'atk-green' : 'atk-yellow')
|
|
137
|
-
return (!page.admin_only) ? '所有人可评' : '管理员可评'
|
|
138
|
-
}
|
|
139
|
-
}).appendTo($adminOnlyBtn)
|
|
140
|
-
|
|
141
|
-
$adminOnlyBtn.onclick = async () => {
|
|
142
|
-
showLoading()
|
|
143
|
-
let p: PageData
|
|
144
|
-
try {
|
|
145
|
-
p = await new Api(this.ctx).pageEdit({ ...page, admin_only: !page.admin_only })
|
|
146
|
-
} catch (err: any) {
|
|
147
|
-
showError(`修改失败:${err.msg || '未知错误'}`)
|
|
148
|
-
console.log(err)
|
|
149
|
-
return
|
|
150
|
-
} finally { hideLoading() }
|
|
151
|
-
page.admin_only = p.admin_only
|
|
152
|
-
adminOnlyActionBtn.updateText()
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// 同步操作
|
|
156
|
-
$syncBtn.onclick = async () => {
|
|
157
|
-
showLoading()
|
|
158
|
-
let p: PageData
|
|
159
|
-
try {
|
|
160
|
-
p = await new Api(this.ctx).pageFetch(page.id)
|
|
161
|
-
} catch (err: any) {
|
|
162
|
-
showError(`同步失败:${err.msg || '未知错误'}`)
|
|
163
|
-
console.log(err)
|
|
164
|
-
return
|
|
165
|
-
} finally { hideLoading() }
|
|
166
|
-
$page.replaceWith(this.renderPage(p))
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
// 删除
|
|
170
|
-
$delBtn.onclick = () => {
|
|
171
|
-
const del = async () => {
|
|
172
|
-
showLoading()
|
|
173
|
-
try {
|
|
174
|
-
await new Api(this.ctx).pageDel(page.key, page.site_name)
|
|
175
|
-
} catch (err: any) {
|
|
176
|
-
console.log(err)
|
|
177
|
-
showError(`删除失败 ${String(err)}`)
|
|
178
|
-
return
|
|
179
|
-
} finally { hideLoading() }
|
|
180
|
-
$page.remove()
|
|
181
|
-
}
|
|
182
|
-
if (window.confirm(
|
|
183
|
-
`确认删除页面 "${page.title || page.key}"?将会删除所有相关数据`
|
|
184
|
-
)) del()
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
/** 关闭编辑器 */
|
|
189
|
-
public closeEditor() {
|
|
190
|
-
if (!this.$editor) return
|
|
191
|
-
|
|
192
|
-
this.$editor.remove()
|
|
193
|
-
}
|
|
194
|
-
}
|
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
import '@/style/site-list.less'
|
|
2
|
-
|
|
3
|
-
import Context from '@/context'
|
|
4
|
-
import * as Utils from '@/lib/utils'
|
|
5
|
-
import * as Ui from '@/lib/ui'
|
|
6
|
-
import { SiteData } from '~/types/artalk-data'
|
|
7
|
-
import Api from '~/src/api'
|
|
8
|
-
|
|
9
|
-
interface SiteListFloaterConf {
|
|
10
|
-
onSwitchSite: (siteName: string) => boolean|void
|
|
11
|
-
onClickSitesViewBtn: () => void
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export default class SiteListFloater {
|
|
15
|
-
ctx: Context
|
|
16
|
-
conf: SiteListFloaterConf
|
|
17
|
-
$el: HTMLElement
|
|
18
|
-
sites: SiteData[] = []
|
|
19
|
-
$sites: HTMLElement
|
|
20
|
-
|
|
21
|
-
constructor(ctx: Context, conf: SiteListFloaterConf) {
|
|
22
|
-
this.ctx = ctx
|
|
23
|
-
this.conf = conf
|
|
24
|
-
this.$el = Utils.createElement(
|
|
25
|
-
`<div class="atk-site-list-floater" style="display: none;">
|
|
26
|
-
<div class="atk-sites"></div>
|
|
27
|
-
</div>`)
|
|
28
|
-
this.$sites = this.$el.querySelector('.atk-sites')!
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/** 装载 */
|
|
32
|
-
public async load(selectedSite?: string) {
|
|
33
|
-
this.$sites.innerHTML = ''
|
|
34
|
-
|
|
35
|
-
const renderSiteItem = (siteName: string, siteLogo: string, siteTarget?: string, onclick?: Function) => {
|
|
36
|
-
const $site = Utils.createElement(
|
|
37
|
-
`<div class="atk-site-item">
|
|
38
|
-
<div class="atk-site-logo"></div>
|
|
39
|
-
<div class="atk-site-name"></div>
|
|
40
|
-
</div>`)
|
|
41
|
-
$site.onclick = !onclick ? () => this.switch(siteTarget || siteName) : () => onclick()
|
|
42
|
-
$site.setAttribute('data-name', siteTarget || siteName)
|
|
43
|
-
const $siteLogo = $site.querySelector<HTMLElement>('.atk-site-logo')!
|
|
44
|
-
const $siteName = $site.querySelector<HTMLElement>('.atk-site-name')!
|
|
45
|
-
$siteLogo.innerText = siteLogo
|
|
46
|
-
$siteName.innerText = siteName
|
|
47
|
-
if (selectedSite === (siteTarget || siteName)) $site.classList.add('atk-active')
|
|
48
|
-
this.$sites.append($site)
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
renderSiteItem('所有站点', '_', '__ATK_SITE_ALL')
|
|
52
|
-
|
|
53
|
-
const sites = await new Api(this.ctx).siteGet()
|
|
54
|
-
sites.forEach((site) => {
|
|
55
|
-
renderSiteItem(site.name, site.name.substr(0, 1))
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
renderSiteItem('站点管理', '+', '', () => { this.conf.onClickSitesViewBtn();this.hide() })
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/** 切换站点 */
|
|
62
|
-
private switch(siteName: string) {
|
|
63
|
-
if (this.conf.onSwitchSite(siteName) === false) { return }
|
|
64
|
-
|
|
65
|
-
// set active
|
|
66
|
-
this.$sites.querySelectorAll('.atk-site-item').forEach(e => {
|
|
67
|
-
if (e.getAttribute('data-name') !== siteName) {
|
|
68
|
-
e.classList.remove('atk-active')
|
|
69
|
-
} else {
|
|
70
|
-
e.classList.add('atk-active')
|
|
71
|
-
}
|
|
72
|
-
})
|
|
73
|
-
|
|
74
|
-
this.hide()
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
private outsideChecker?: (evt: MouseEvent) => void // outside checker
|
|
78
|
-
|
|
79
|
-
/** 显示 */
|
|
80
|
-
public show($trigger?: HTMLElement) {
|
|
81
|
-
this.$el.style.display = ''
|
|
82
|
-
|
|
83
|
-
// 点击外部隐藏
|
|
84
|
-
if ($trigger) {
|
|
85
|
-
this.outsideChecker = (evt: MouseEvent) => {
|
|
86
|
-
const isClickInside = $trigger.contains(evt.target as any)||this.$el.contains(evt.target as any)
|
|
87
|
-
if (!isClickInside) { this.hide() }
|
|
88
|
-
}
|
|
89
|
-
document.addEventListener('click', this.outsideChecker)
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/** 隐藏 */
|
|
94
|
-
public hide() {
|
|
95
|
-
this.$el.style.display = 'none'
|
|
96
|
-
if (this.outsideChecker) document.removeEventListener('click', this.outsideChecker)
|
|
97
|
-
}
|
|
98
|
-
}
|
|
@@ -1,289 +0,0 @@
|
|
|
1
|
-
import '@/style/site-list.less'
|
|
2
|
-
|
|
3
|
-
import Context from '@/context'
|
|
4
|
-
import Component from '@/lib/component'
|
|
5
|
-
import * as Utils from '@/lib/utils'
|
|
6
|
-
import * as Ui from '@/lib/ui'
|
|
7
|
-
import { SiteData } from '~/types/artalk-data'
|
|
8
|
-
import ItemTextEditor from '../item-text-editor'
|
|
9
|
-
import Api from '~/src/api'
|
|
10
|
-
|
|
11
|
-
export default class SiteList extends Component {
|
|
12
|
-
sites: SiteData[] = []
|
|
13
|
-
|
|
14
|
-
$header: HTMLElement
|
|
15
|
-
$headerTitle: HTMLElement
|
|
16
|
-
$headerActions: HTMLElement
|
|
17
|
-
$rowsWrap: HTMLElement
|
|
18
|
-
|
|
19
|
-
$editor?: HTMLElement
|
|
20
|
-
activeSite: string = ''
|
|
21
|
-
|
|
22
|
-
$add?: HTMLElement
|
|
23
|
-
|
|
24
|
-
constructor(ctx: Context) {
|
|
25
|
-
super(ctx)
|
|
26
|
-
|
|
27
|
-
this.$el = Utils.createElement(
|
|
28
|
-
`<div class="atk-site-list">
|
|
29
|
-
<div class="atk-header">
|
|
30
|
-
<div class="atk-title"></div>
|
|
31
|
-
<div class="atk-actions">
|
|
32
|
-
<div class="atk-item atk-site-add-btn"><i class="atk-icon atk-icon-plus"></i></div>
|
|
33
|
-
</div>
|
|
34
|
-
</div>
|
|
35
|
-
<div class="atk-site-rows-wrap"></div>
|
|
36
|
-
</div>`)
|
|
37
|
-
|
|
38
|
-
this.$header = this.$el.querySelector('.atk-header')!
|
|
39
|
-
this.$headerTitle = this.$header.querySelector('.atk-title')!
|
|
40
|
-
this.$headerActions = this.$header.querySelector('.atk-actions')!
|
|
41
|
-
this.$rowsWrap = this.$el.querySelector('.atk-site-rows-wrap')!
|
|
42
|
-
|
|
43
|
-
// 标题
|
|
44
|
-
this.$headerTitle.innerText = `共 0 个站点`
|
|
45
|
-
|
|
46
|
-
// 新建站点按钮
|
|
47
|
-
const $addBtn = this.$headerActions.querySelector<HTMLElement>('.atk-site-add-btn')!
|
|
48
|
-
$addBtn.onclick = () => {
|
|
49
|
-
this.closeEditor()
|
|
50
|
-
this.showAdd()
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
public loadSites(sites: SiteData[]) {
|
|
55
|
-
this.sites = sites
|
|
56
|
-
this.activeSite = ''
|
|
57
|
-
this.$rowsWrap.innerHTML = ''
|
|
58
|
-
this.$headerTitle.innerText = `共 0 个站点`
|
|
59
|
-
|
|
60
|
-
let $row: HTMLElement
|
|
61
|
-
|
|
62
|
-
for (let i = 0; i < sites.length; i++) {
|
|
63
|
-
const site = sites[i]
|
|
64
|
-
|
|
65
|
-
if (i % 4 === 0) { // 每 4 个 item 为一行
|
|
66
|
-
$row = Utils.createElement('<div class="atk-site-row">')
|
|
67
|
-
this.$rowsWrap.append($row)
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// 创建 site item
|
|
71
|
-
const $site = this.renderSite(site, $row!)
|
|
72
|
-
$row!.append($site)
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
this.$headerTitle.innerText = `共 ${sites.length} 个站点`
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
public renderSite(site: SiteData, $row: HTMLElement) {
|
|
79
|
-
const $site = Utils.createElement(
|
|
80
|
-
`<div class="atk-site-item">
|
|
81
|
-
<div class="atk-site-logo"></div>
|
|
82
|
-
<div class="atk-site-name"></div>
|
|
83
|
-
</div>`)
|
|
84
|
-
|
|
85
|
-
const $siteLogo = $site.querySelector<HTMLElement>('.atk-site-logo')!
|
|
86
|
-
const $siteName = $site.querySelector<HTMLElement>('.atk-site-name')!
|
|
87
|
-
const setActive = () => { $site.classList.add('atk-active') }
|
|
88
|
-
|
|
89
|
-
$siteLogo.innerText = site.name.substr(0, 1)
|
|
90
|
-
$siteName.innerText = site.name
|
|
91
|
-
|
|
92
|
-
// click
|
|
93
|
-
$site.onclick = () => {
|
|
94
|
-
this.closeEditor()
|
|
95
|
-
this.closeAdd()
|
|
96
|
-
setActive()
|
|
97
|
-
this.showEditor(site, $site, $row)
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
if (this.activeSite === site.name) { setActive() }
|
|
101
|
-
|
|
102
|
-
return $site
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
public showEditor(site: SiteData, $site: HTMLElement, $row: HTMLElement) {
|
|
106
|
-
this.activeSite = site.name
|
|
107
|
-
this.$editor = Utils.createElement(`
|
|
108
|
-
<div class="atk-site-edit">
|
|
109
|
-
<div class="atk-header">
|
|
110
|
-
<div class="atk-site-info">
|
|
111
|
-
<span class="atk-site-name"></span>
|
|
112
|
-
<span class="atk-site-urls"></span>
|
|
113
|
-
</div>
|
|
114
|
-
<div class="atk-close-btn">
|
|
115
|
-
<i class="atk-icon atk-icon-close"></i>
|
|
116
|
-
</div>
|
|
117
|
-
</div>
|
|
118
|
-
<div class="atk-main">
|
|
119
|
-
<div class="atk-site-text-actions">
|
|
120
|
-
<div class="atk-item atk-rename-btn">重命名</div>
|
|
121
|
-
<div class="atk-item atk-edit-url-btn">修改 URL</div>
|
|
122
|
-
<!--<div class="atk-item atk-export-btn">导出</div>
|
|
123
|
-
<div class="atk-item atk-import-btn">导入</div>-->
|
|
124
|
-
</div>
|
|
125
|
-
<div class="atk-site-btn-actions">
|
|
126
|
-
<div class="atk-item atk-del-btn">
|
|
127
|
-
<i class="atk-icon atk-icon-del"></i>
|
|
128
|
-
</div>
|
|
129
|
-
</div>
|
|
130
|
-
</div>
|
|
131
|
-
</div>`)
|
|
132
|
-
|
|
133
|
-
// 插入
|
|
134
|
-
$row.before(this.$editor)
|
|
135
|
-
|
|
136
|
-
// header
|
|
137
|
-
const $siteName = this.$editor.querySelector<HTMLElement>('.atk-site-name')!
|
|
138
|
-
const $siteUrls = this.$editor.querySelector<HTMLElement>('.atk-site-urls')!
|
|
139
|
-
const $closeBtn = this.$editor.querySelector<HTMLElement>('.atk-close-btn')!
|
|
140
|
-
$closeBtn.onclick = () => this.closeEditor()
|
|
141
|
-
|
|
142
|
-
const update = (s: SiteData) => {
|
|
143
|
-
site = s
|
|
144
|
-
$siteName.innerText = site.name
|
|
145
|
-
$siteName.onclick = () => { if (site.first_url) window.open(site.first_url) }
|
|
146
|
-
$siteUrls.innerHTML = ''
|
|
147
|
-
site.urls?.forEach(u => {
|
|
148
|
-
const $item = Utils.createElement('<span class="atk-url-item"></span>')
|
|
149
|
-
$siteUrls.append($item)
|
|
150
|
-
$item.innerText = (u || '').replace(/\/$/, '')
|
|
151
|
-
$item.onclick = () => { window.open(u) }
|
|
152
|
-
})
|
|
153
|
-
}
|
|
154
|
-
update(site)
|
|
155
|
-
|
|
156
|
-
// actions
|
|
157
|
-
const $main = this.$editor.querySelector<HTMLElement>('.atk-main')!
|
|
158
|
-
const $actions = this.$editor.querySelector<HTMLElement>('.atk-site-text-actions')!
|
|
159
|
-
const $renameBtn = $actions.querySelector<HTMLElement>('.atk-rename-btn')!
|
|
160
|
-
const $editUrlBtn = $actions.querySelector<HTMLElement>('.atk-edit-url-btn')!
|
|
161
|
-
// const $exportBtn = $actions.querySelector<HTMLElement>('.atk-export-btn')!
|
|
162
|
-
// const $importBtn = $actions.querySelector<HTMLElement>(`.atk-import-btn`)!
|
|
163
|
-
const $delBtn = this.$editor.querySelector<HTMLElement>('.atk-del-btn')!
|
|
164
|
-
const showLoading = () => { Ui.showLoading(this.$editor!) }
|
|
165
|
-
const hideLoading = () => { Ui.hideLoading(this.$editor!) }
|
|
166
|
-
const showError = (msg: string) => { window.alert(msg) }
|
|
167
|
-
|
|
168
|
-
// 文本编辑
|
|
169
|
-
const openTextEditor = (key: string) => {
|
|
170
|
-
let initValue = site[key] || ''
|
|
171
|
-
if (key === 'urls') initValue = site.urls_raw || ''
|
|
172
|
-
const textEditor = new ItemTextEditor({
|
|
173
|
-
initValue,
|
|
174
|
-
onYes: async (val: string) => {
|
|
175
|
-
Ui.showLoading(textEditor.$el)
|
|
176
|
-
let s: SiteData
|
|
177
|
-
try {
|
|
178
|
-
s = await new Api(this.ctx).siteEdit({ ...site, [key]: val })
|
|
179
|
-
} catch (err: any) {
|
|
180
|
-
showError(`修改失败:${err.msg || '未知错误'}`)
|
|
181
|
-
console.error(err)
|
|
182
|
-
return false
|
|
183
|
-
} finally { Ui.hideLoading(textEditor.$el) }
|
|
184
|
-
$site.replaceWith(this.renderSite(s, $row))
|
|
185
|
-
update(s)
|
|
186
|
-
return true
|
|
187
|
-
}
|
|
188
|
-
})
|
|
189
|
-
textEditor.appendTo($main)
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
// 重命名
|
|
193
|
-
$renameBtn.onclick = () => openTextEditor('name')
|
|
194
|
-
|
|
195
|
-
// 修改 URL
|
|
196
|
-
$editUrlBtn.onclick = () => openTextEditor('urls')
|
|
197
|
-
|
|
198
|
-
// 导出
|
|
199
|
-
// $exportBtn.onclick = () => {}
|
|
200
|
-
|
|
201
|
-
// // 导入
|
|
202
|
-
// $importBtn.onclick = () => {}
|
|
203
|
-
|
|
204
|
-
// 删除
|
|
205
|
-
$delBtn.onclick = () => {
|
|
206
|
-
const del = async () => {
|
|
207
|
-
showLoading()
|
|
208
|
-
try {
|
|
209
|
-
await new Api(this.ctx).siteDel(site.id, true)
|
|
210
|
-
} catch (err: any) {
|
|
211
|
-
console.log(err)
|
|
212
|
-
showError(`删除失败 ${String(err)}`)
|
|
213
|
-
return
|
|
214
|
-
} finally { hideLoading() }
|
|
215
|
-
this.closeEditor()
|
|
216
|
-
$site.remove()
|
|
217
|
-
this.sites = this.sites.filter(s => s.name !== site.name)
|
|
218
|
-
}
|
|
219
|
-
if (window.confirm(
|
|
220
|
-
`确认删除站点 "${site.name}"?将会删除所有相关数据`
|
|
221
|
-
)) del()
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
public closeEditor() {
|
|
226
|
-
if (!this.$editor) return
|
|
227
|
-
|
|
228
|
-
this.$editor.remove()
|
|
229
|
-
this.$rowsWrap.querySelectorAll('.atk-site-item').forEach((e) => e.classList.remove('atk-active'))
|
|
230
|
-
this.activeSite = ''
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
public showAdd() {
|
|
234
|
-
this.closeAdd()
|
|
235
|
-
|
|
236
|
-
this.$add = Utils.createElement(`
|
|
237
|
-
<div class="atk-site-add">
|
|
238
|
-
<div class="atk-header">
|
|
239
|
-
<div class="atk-title">新增站点</div>
|
|
240
|
-
<div class="atk-close-btn">
|
|
241
|
-
<i class="atk-icon atk-icon-close"></i>
|
|
242
|
-
</div>
|
|
243
|
-
</div>
|
|
244
|
-
<div class="atk-form">
|
|
245
|
-
<input type="text" name="AtkSiteName" placeholder="站点名称" autocomplete="off">
|
|
246
|
-
<input type="text" name="AtkSiteUrls" placeholder="站点 URL(多个用逗号隔开)" autocomplete="off">
|
|
247
|
-
<button class="atk-btn" name="AtkSubmit">创建</button>
|
|
248
|
-
</div>
|
|
249
|
-
</div>`)
|
|
250
|
-
this.$header.after(this.$add)
|
|
251
|
-
|
|
252
|
-
const $closeBtn = this.$add.querySelector<HTMLElement>('.atk-close-btn')!
|
|
253
|
-
$closeBtn.onclick = () => this.closeAdd()
|
|
254
|
-
|
|
255
|
-
const $siteName = this.$add.querySelector<HTMLInputElement>('[name="AtkSiteName"]')!
|
|
256
|
-
const $siteUrls = this.$add.querySelector<HTMLInputElement>('[name="AtkSiteUrls"]')!
|
|
257
|
-
const $submitBtn = this.$add.querySelector<HTMLButtonElement>('[name="AtkSubmit"]')!
|
|
258
|
-
|
|
259
|
-
$submitBtn.onclick = async () => {
|
|
260
|
-
const siteName = $siteName.value.trim()
|
|
261
|
-
const siteUrls = $siteUrls.value.trim()
|
|
262
|
-
|
|
263
|
-
if (siteName === '') { $siteName.focus(); return }
|
|
264
|
-
|
|
265
|
-
Ui.showLoading(this.$add!)
|
|
266
|
-
let s: SiteData
|
|
267
|
-
try {
|
|
268
|
-
s = await new Api(this.ctx).siteAdd(siteName, siteUrls)
|
|
269
|
-
} catch (err: any) {
|
|
270
|
-
window.alert(`创建失败:${err.msg || ''}`)
|
|
271
|
-
console.error(err)
|
|
272
|
-
return
|
|
273
|
-
} finally { Ui.hideLoading(this.$add!) }
|
|
274
|
-
|
|
275
|
-
this.sites.push(s)
|
|
276
|
-
this.loadSites(this.sites)
|
|
277
|
-
this.closeAdd()
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
// 回车键提交
|
|
281
|
-
const keyDown = (evt: KeyboardEvent) => { if (evt.key === 'Enter') { $submitBtn.click() } }
|
|
282
|
-
$siteName.onkeyup = (evt) => keyDown(evt)
|
|
283
|
-
$siteUrls.onkeyup = (evt) => keyDown(evt)
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
public closeAdd() {
|
|
287
|
-
this.$add?.remove()
|
|
288
|
-
}
|
|
289
|
-
}
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
<div class="atk-sidebar">
|
|
2
|
-
<div class="atk-sidebar-inner">
|
|
3
|
-
<div class="atk-sidebar-header">
|
|
4
|
-
<span class="atk-avatar">
|
|
5
|
-
<span class="atk-site-logo"></span>
|
|
6
|
-
</span>
|
|
7
|
-
<span class="atk-menu">
|
|
8
|
-
<span class="atk-item atk-active atk-sidebar-title">控制中心</span>
|
|
9
|
-
</span>
|
|
10
|
-
<div class="atk-sidebar-close"><i class="atk-icon atk-icon-close"></i></div>
|
|
11
|
-
</div>
|
|
12
|
-
<div class="atk-sidebar-nav">
|
|
13
|
-
<div class="akt-curt-view-btn">
|
|
14
|
-
<div class="atk-icon"><span></span><span></span><span></span></div>
|
|
15
|
-
<div class="atk-text"></div>
|
|
16
|
-
</div>
|
|
17
|
-
<div class="atk-tabs"></div>
|
|
18
|
-
<div class="atk-tabs atk-views" style="display: none;"></div>
|
|
19
|
-
</div>
|
|
20
|
-
<div class="atk-sidebar-view-wrap"></div>
|
|
21
|
-
</div>
|
|
22
|
-
</div>
|