@esershnr/artalk-sidebar 1.0.3
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 +16 -0
- package/auto-imports.d.ts +76 -0
- package/components.d.ts +31 -0
- package/env.d.ts +2 -0
- package/index.html +15 -0
- package/package.json +32 -0
- package/public/favicon.png +0 -0
- package/public/robots.txt +2 -0
- package/src/App.vue +89 -0
- package/src/artalk.ts +82 -0
- package/src/assets/favicon.png +0 -0
- package/src/assets/icon-darkmode-off.svg +1 -0
- package/src/assets/icon-darkmode-on.svg +1 -0
- package/src/assets/icon-eye-off.svg +1 -0
- package/src/assets/icon-eye-on.svg +1 -0
- package/src/assets/nav-icon-comments.svg +1 -0
- package/src/assets/nav-icon-pages.svg +1 -0
- package/src/assets/nav-icon-search.svg +1 -0
- package/src/assets/nav-icon-settings.svg +1 -0
- package/src/assets/nav-icon-sites.svg +1 -0
- package/src/assets/nav-icon-transfer.svg +1 -0
- package/src/assets/nav-icon-users.svg +1 -0
- package/src/components/AppHeader.vue +235 -0
- package/src/components/AppNavigation.vue +11 -0
- package/src/components/AppNavigationDesktop.vue +176 -0
- package/src/components/AppNavigationMenu.ts +152 -0
- package/src/components/AppNavigationMobile.vue +187 -0
- package/src/components/AppNavigationSearch.vue +137 -0
- package/src/components/FileUploader.vue +149 -0
- package/src/components/ItemTextEditor.vue +130 -0
- package/src/components/LoadingLayer.vue +37 -0
- package/src/components/LogTerminal.vue +89 -0
- package/src/components/PageEditor.vue +171 -0
- package/src/components/Pagination.vue +253 -0
- package/src/components/PreferenceArr.vue +105 -0
- package/src/components/PreferenceGrp.vue +153 -0
- package/src/components/PreferenceItem.vue +159 -0
- package/src/components/SiteCreate.vue +96 -0
- package/src/components/SiteEditor.vue +138 -0
- package/src/components/SiteSwitcher.vue +184 -0
- package/src/components/UserEditor.vue +229 -0
- package/src/global.ts +62 -0
- package/src/hooks/MobileWidth.ts +27 -0
- package/src/i18n/fr.ts +103 -0
- package/src/i18n/ja.ts +100 -0
- package/src/i18n/ko.ts +99 -0
- package/src/i18n/ru.ts +102 -0
- package/src/i18n/tr.ts +102 -0
- package/src/i18n/zh-CN.ts +97 -0
- package/src/i18n/zh-TW.ts +97 -0
- package/src/i18n-en.ts +99 -0
- package/src/i18n.ts +37 -0
- package/src/lib/promise-polyfill.ts +9 -0
- package/src/lib/settings-option.ts +186 -0
- package/src/lib/settings-sensitive.ts +44 -0
- package/src/lib/settings.ts +94 -0
- package/src/main.ts +65 -0
- package/src/pages/comments.vue +110 -0
- package/src/pages/index.vue +33 -0
- package/src/pages/login.vue +245 -0
- package/src/pages/pages.vue +309 -0
- package/src/pages/settings.vue +181 -0
- package/src/pages/sites.vue +353 -0
- package/src/pages/transfer.vue +204 -0
- package/src/pages/users.vue +271 -0
- package/src/stores/nav.ts +114 -0
- package/src/stores/user.ts +48 -0
- package/src/style/_extends.scss +100 -0
- package/src/style/_variables.scss +18 -0
- package/src/style.scss +245 -0
- package/src/vue-i18n.d.ts +7 -0
- package/tsconfig.json +40 -0
- package/tsconfig.node.json +11 -0
- package/typed-router.d.ts +30 -0
- package/vite.config.ts +71 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { storeToRefs } from 'pinia'
|
|
3
|
+
import { useNavStore } from '../stores/nav'
|
|
4
|
+
import { useUserStore } from '../stores/user'
|
|
5
|
+
import { artalk } from '../global'
|
|
6
|
+
|
|
7
|
+
const nav = useNavStore()
|
|
8
|
+
const user = useUserStore()
|
|
9
|
+
const { curtTab } = storeToRefs(nav)
|
|
10
|
+
const { t } = useI18n()
|
|
11
|
+
|
|
12
|
+
const importParams = ref({
|
|
13
|
+
siteName: '',
|
|
14
|
+
siteURL: '',
|
|
15
|
+
payload: '',
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
const isLoading = ref(false)
|
|
19
|
+
|
|
20
|
+
const uploadApiURL = ref('')
|
|
21
|
+
const importTaskApiURL = ref('')
|
|
22
|
+
|
|
23
|
+
const uploadedFilename = ref('')
|
|
24
|
+
|
|
25
|
+
const importTaskStarted = ref(false)
|
|
26
|
+
const importTaskParams = ref<Record<string, string>>({})
|
|
27
|
+
|
|
28
|
+
const exportTaskStarted = ref(false)
|
|
29
|
+
|
|
30
|
+
onMounted(() => {
|
|
31
|
+
nav.updateTabs(
|
|
32
|
+
{
|
|
33
|
+
import: 'import',
|
|
34
|
+
export: 'export',
|
|
35
|
+
},
|
|
36
|
+
'import',
|
|
37
|
+
)
|
|
38
|
+
watch(curtTab, (tab) => {
|
|
39
|
+
if (tab === 'export') {
|
|
40
|
+
startExportTask()
|
|
41
|
+
curtTab.value = 'import'
|
|
42
|
+
}
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
uploadApiURL.value = `${artalk?.ctx.conf.server}/api/v2/transfer/upload`
|
|
46
|
+
importTaskApiURL.value = `${artalk?.ctx.conf.server}/api/v2/transfer/import`
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
function setError(msg: string) {
|
|
50
|
+
window.alert(msg)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function fileUploaded(filename: string) {
|
|
54
|
+
uploadedFilename.value = filename
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function startImportTask() {
|
|
58
|
+
if (!uploadedFilename.value) {
|
|
59
|
+
setError(`Please upload a data file first`)
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const p = importParams.value
|
|
64
|
+
const siteName = p.siteName.trim()
|
|
65
|
+
const siteURL = p.siteURL.trim()
|
|
66
|
+
const payload = p.payload.trim()
|
|
67
|
+
|
|
68
|
+
// Prepare request params
|
|
69
|
+
let rData: any = {}
|
|
70
|
+
if (payload) {
|
|
71
|
+
// Validate payload JSON
|
|
72
|
+
try {
|
|
73
|
+
rData = JSON.parse(payload)
|
|
74
|
+
} catch (err) {
|
|
75
|
+
setError(`Payload JSON invalid: ${err}`)
|
|
76
|
+
return
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (typeof rData !== 'object' || Array.isArray(rData)) {
|
|
80
|
+
setError(`Payload should be an object`)
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (siteName) rData.target_site_name = siteName
|
|
85
|
+
if (siteURL) rData.target_site_url = siteURL
|
|
86
|
+
rData.json_file = uploadedFilename.value
|
|
87
|
+
|
|
88
|
+
// Create an import task
|
|
89
|
+
importTaskParams.value = {
|
|
90
|
+
...rData,
|
|
91
|
+
token: user.token,
|
|
92
|
+
}
|
|
93
|
+
importTaskStarted.value = true
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function importTaskDone() {
|
|
97
|
+
importTaskStarted.value = false
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function startExportTask() {
|
|
101
|
+
if (exportTaskStarted.value) return
|
|
102
|
+
exportTaskStarted.value = true
|
|
103
|
+
isLoading.value = true
|
|
104
|
+
try {
|
|
105
|
+
const res = await artalk!.ctx.getApi().transfer.exportArtrans()
|
|
106
|
+
downloadFile(`backup-${getYmdHisFilename()}.artrans`, res.data.artrans)
|
|
107
|
+
} catch (err: any) {
|
|
108
|
+
console.error(err)
|
|
109
|
+
window.alert(`${String(err)}`)
|
|
110
|
+
return
|
|
111
|
+
} finally {
|
|
112
|
+
exportTaskStarted.value = false
|
|
113
|
+
isLoading.value = false
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function downloadFile(filename: string, text: string) {
|
|
118
|
+
const el = document.createElement('a')
|
|
119
|
+
el.setAttribute('href', `data:text/json;charset=utf-8,${encodeURIComponent(text)}`)
|
|
120
|
+
el.setAttribute('download', filename)
|
|
121
|
+
el.style.display = 'none'
|
|
122
|
+
document.body.appendChild(el)
|
|
123
|
+
el.click()
|
|
124
|
+
document.body.removeChild(el)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function getYmdHisFilename() {
|
|
128
|
+
const date = new Date()
|
|
129
|
+
|
|
130
|
+
const year = date.getFullYear()
|
|
131
|
+
const month = date.getMonth() + 1
|
|
132
|
+
const day = date.getDate()
|
|
133
|
+
const hours = date.getHours()
|
|
134
|
+
const minutes = date.getMinutes()
|
|
135
|
+
const seconds = date.getSeconds()
|
|
136
|
+
|
|
137
|
+
return `${year}${month}${day}-${hours}${padWithZeros(minutes, 2)}${padWithZeros(seconds, 2)}`
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function padWithZeros(vNumber: number, width: number) {
|
|
141
|
+
let numAsString = vNumber.toString()
|
|
142
|
+
while (numAsString.length < width) {
|
|
143
|
+
numAsString = `0${numAsString}`
|
|
144
|
+
}
|
|
145
|
+
return numAsString
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const artransferToolHint = computed(() =>
|
|
149
|
+
t('artransferToolHint', { link: '__LINK__' }).replace(
|
|
150
|
+
'__LINK__',
|
|
151
|
+
`<a href="https://artalk.js.org/guide/transfer.html" target="_blank">${t('artransfer')}</a>`,
|
|
152
|
+
),
|
|
153
|
+
)
|
|
154
|
+
</script>
|
|
155
|
+
|
|
156
|
+
<template>
|
|
157
|
+
<LoadingLayer v-if="isLoading" />
|
|
158
|
+
<LogTerminal
|
|
159
|
+
v-if="importTaskStarted"
|
|
160
|
+
:api-url="importTaskApiURL"
|
|
161
|
+
:req-params="importTaskParams"
|
|
162
|
+
@back="importTaskDone()"
|
|
163
|
+
/>
|
|
164
|
+
<div v-show="!importTaskStarted" class="atk-form">
|
|
165
|
+
<div class="atk-label atk-data-file-label">Artrans {{ t('dataFile') }}</div>
|
|
166
|
+
<FileUploader :api-url="uploadApiURL" @done="fileUploaded">
|
|
167
|
+
<template #tip>
|
|
168
|
+
<!-- eslint-disable-next-line vue/no-v-html -->
|
|
169
|
+
<span v-html="artransferToolHint" />
|
|
170
|
+
</template>
|
|
171
|
+
<template #done-msg>
|
|
172
|
+
{{ t('uploadReadyToImport') }}
|
|
173
|
+
</template>
|
|
174
|
+
</FileUploader>
|
|
175
|
+
<div class="atk-label">{{ t('targetSiteName') }}</div>
|
|
176
|
+
<input
|
|
177
|
+
v-model="importParams.siteName"
|
|
178
|
+
type="text"
|
|
179
|
+
name="AtkSiteName"
|
|
180
|
+
:placeholder="t('inputHint')"
|
|
181
|
+
autocomplete="off"
|
|
182
|
+
/>
|
|
183
|
+
<div class="atk-label">{{ t('targetSiteURL') }}</div>
|
|
184
|
+
<input
|
|
185
|
+
v-model="importParams.siteURL"
|
|
186
|
+
type="text"
|
|
187
|
+
name="AtkSiteURL"
|
|
188
|
+
:placeholder="t('inputHint')"
|
|
189
|
+
autocomplete="off"
|
|
190
|
+
/>
|
|
191
|
+
<div class="atk-label">{{ t('payload') }} ({{ t('optional') }})</div>
|
|
192
|
+
<textarea v-model="importParams.payload" name="AtkPayload"></textarea>
|
|
193
|
+
<span class="atk-desc">
|
|
194
|
+
<a href="https://artalk.js.org/guide/transfer.html" target="_blank">
|
|
195
|
+
{{ t('moreDetails') }}
|
|
196
|
+
</a>
|
|
197
|
+
</span>
|
|
198
|
+
<button class="atk-btn" name="AtkSubmit" @click="startImportTask()">
|
|
199
|
+
{{ t('import') }}
|
|
200
|
+
</button>
|
|
201
|
+
</div>
|
|
202
|
+
</template>
|
|
203
|
+
|
|
204
|
+
<style scoped lang="scss"></style>
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { ArtalkType } from '@esershnr/artalk'
|
|
3
|
+
import { storeToRefs } from 'pinia'
|
|
4
|
+
import { useNavStore } from '../stores/nav'
|
|
5
|
+
import { artalk } from '../global'
|
|
6
|
+
import Pagination from '../components/Pagination.vue'
|
|
7
|
+
|
|
8
|
+
const nav = useNavStore()
|
|
9
|
+
const { curtTab } = storeToRefs(nav)
|
|
10
|
+
const users = ref<ArtalkType.UserDataForAdmin[]>([])
|
|
11
|
+
const { t } = useI18n()
|
|
12
|
+
|
|
13
|
+
const pageSize = ref(30)
|
|
14
|
+
const pageTotal = ref(0)
|
|
15
|
+
const pagination = ref<InstanceType<typeof Pagination>>()
|
|
16
|
+
const curtType = ref<'all' | 'admin' | 'in_conf' | undefined>('all')
|
|
17
|
+
|
|
18
|
+
const userEditorState = reactive({
|
|
19
|
+
show: false,
|
|
20
|
+
user: undefined as ArtalkType.UserDataForAdmin | undefined,
|
|
21
|
+
})
|
|
22
|
+
const search = ref('')
|
|
23
|
+
|
|
24
|
+
watch(curtTab, (tab) => {
|
|
25
|
+
if (tab === 'create') {
|
|
26
|
+
createUser()
|
|
27
|
+
} else {
|
|
28
|
+
curtType.value = tab as any
|
|
29
|
+
fetchUsers(0)
|
|
30
|
+
closeUserEditor()
|
|
31
|
+
}
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
onMounted(() => {
|
|
35
|
+
fetchUsers(0)
|
|
36
|
+
|
|
37
|
+
nav.updateTabs(
|
|
38
|
+
{
|
|
39
|
+
all: 'all',
|
|
40
|
+
admin: 'admin',
|
|
41
|
+
create: 'create',
|
|
42
|
+
},
|
|
43
|
+
'all',
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
// Users search
|
|
47
|
+
nav.enableSearch(
|
|
48
|
+
(value: string) => {
|
|
49
|
+
search.value = value
|
|
50
|
+
fetchUsers(0)
|
|
51
|
+
},
|
|
52
|
+
() => {
|
|
53
|
+
if (search.value === '') return
|
|
54
|
+
search.value = ''
|
|
55
|
+
fetchUsers(0)
|
|
56
|
+
},
|
|
57
|
+
)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
watch(
|
|
61
|
+
() => userEditorState.show,
|
|
62
|
+
() => nav.scrollPageToTop(),
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
function fetchUsers(offset: number) {
|
|
66
|
+
if (offset === 0) pagination.value?.reset()
|
|
67
|
+
nav.setPageLoading(true)
|
|
68
|
+
artalk?.ctx
|
|
69
|
+
.getApi()
|
|
70
|
+
.users.getUsers(curtType.value, {
|
|
71
|
+
offset,
|
|
72
|
+
limit: pageSize.value,
|
|
73
|
+
search: search.value,
|
|
74
|
+
})
|
|
75
|
+
.then((res) => {
|
|
76
|
+
pageTotal.value = res.data.count
|
|
77
|
+
users.value = res.data.users
|
|
78
|
+
nav.scrollPageToTop()
|
|
79
|
+
})
|
|
80
|
+
.finally(() => {
|
|
81
|
+
nav.setPageLoading(false)
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function onChangePage(offset: number) {
|
|
86
|
+
fetchUsers(offset)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function editUser(user: ArtalkType.UserDataForAdmin) {
|
|
90
|
+
if (user.is_in_conf) {
|
|
91
|
+
alert(t('userInConfCannotEditHint'))
|
|
92
|
+
return
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
userEditorState.show = true
|
|
96
|
+
userEditorState.user = user
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function createUser() {
|
|
100
|
+
userEditorState.show = true
|
|
101
|
+
userEditorState.user = undefined
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function updateUser(user: ArtalkType.UserDataForAdmin) {
|
|
105
|
+
const index = users.value.findIndex((u) => u.id === user.id)
|
|
106
|
+
if (index != -1) {
|
|
107
|
+
// Edit user
|
|
108
|
+
const orgUser = users.value[index]
|
|
109
|
+
Object.keys(user).forEach((key) => {
|
|
110
|
+
;(orgUser as any)[key] = (user as any)[key]
|
|
111
|
+
})
|
|
112
|
+
} else {
|
|
113
|
+
// Create user
|
|
114
|
+
fetchUsers(0)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
closeUserEditor()
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function closeUserEditor() {
|
|
121
|
+
userEditorState.show = false
|
|
122
|
+
userEditorState.user = undefined
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function delUser(user: ArtalkType.UserDataForAdmin) {
|
|
126
|
+
if (
|
|
127
|
+
window.confirm(
|
|
128
|
+
t('userDeleteConfirm', {
|
|
129
|
+
name: user.name,
|
|
130
|
+
email: user.email,
|
|
131
|
+
}),
|
|
132
|
+
)
|
|
133
|
+
) {
|
|
134
|
+
artalk!.ctx
|
|
135
|
+
.getApi()
|
|
136
|
+
.users.deleteUser(user.id)
|
|
137
|
+
.then(() => {
|
|
138
|
+
const index = users.value.findIndex((u) => u.id === user.id)
|
|
139
|
+
users.value.splice(index, 1)
|
|
140
|
+
|
|
141
|
+
if (user.is_in_conf) {
|
|
142
|
+
alert(t('userDeleteManuallyHint'))
|
|
143
|
+
}
|
|
144
|
+
})
|
|
145
|
+
.catch((e: ArtalkType.FetchError) => {
|
|
146
|
+
alert(e.message)
|
|
147
|
+
})
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
</script>
|
|
151
|
+
|
|
152
|
+
<template>
|
|
153
|
+
<div class="user-list-wrap">
|
|
154
|
+
<div class="user-list">
|
|
155
|
+
<div v-for="user in users" :key="user.id" class="user-item">
|
|
156
|
+
<div class="user-main">
|
|
157
|
+
<div class="title">
|
|
158
|
+
{{ user.name }}
|
|
159
|
+
<span class="badge-grp">
|
|
160
|
+
<span
|
|
161
|
+
v-if="user.badge_name"
|
|
162
|
+
class="badge"
|
|
163
|
+
:style="{ backgroundColor: user.badge_color }"
|
|
164
|
+
>
|
|
165
|
+
{{ user.badge_name }}
|
|
166
|
+
</span>
|
|
167
|
+
<span v-else-if="user.is_admin" class="badge admin" :title="t('userAdminHint')">
|
|
168
|
+
{{ t('Admin') }}
|
|
169
|
+
</span>
|
|
170
|
+
<span v-if="user.is_in_conf" class="badge in-conf" :title="t('userInConfHint')">
|
|
171
|
+
{{ t('Config') }}
|
|
172
|
+
</span>
|
|
173
|
+
</span>
|
|
174
|
+
</div>
|
|
175
|
+
<div class="sub">{{ user.email }}</div>
|
|
176
|
+
</div>
|
|
177
|
+
<div class="user-actions">
|
|
178
|
+
<span @click="editUser(user)">{{ t('edit') }}</span>
|
|
179
|
+
<span>{{ t('comment') }} ({{ user.comment_count }})</span>
|
|
180
|
+
<span @click="delUser(user)">{{ t('delete') }}</span>
|
|
181
|
+
</div>
|
|
182
|
+
</div>
|
|
183
|
+
</div>
|
|
184
|
+
<Pagination
|
|
185
|
+
ref="pagination"
|
|
186
|
+
:page-size="pageSize"
|
|
187
|
+
:total="pageTotal"
|
|
188
|
+
:disabled="nav.isPageLoading"
|
|
189
|
+
@change="onChangePage"
|
|
190
|
+
/>
|
|
191
|
+
|
|
192
|
+
<UserEditor
|
|
193
|
+
v-if="userEditorState.show"
|
|
194
|
+
:user="userEditorState.user"
|
|
195
|
+
@update="updateUser"
|
|
196
|
+
@close="closeUserEditor"
|
|
197
|
+
/>
|
|
198
|
+
</div>
|
|
199
|
+
</template>
|
|
200
|
+
|
|
201
|
+
<style scoped lang="scss">
|
|
202
|
+
.user-list-wrap {
|
|
203
|
+
.user-list {
|
|
204
|
+
.user-item {
|
|
205
|
+
padding: 15px 30px;
|
|
206
|
+
|
|
207
|
+
&:not(:last-child) {
|
|
208
|
+
border-bottom: 1px solid var(--at-color-border);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
.user-main {
|
|
212
|
+
.title {
|
|
213
|
+
display: flex;
|
|
214
|
+
flex-direction: row;
|
|
215
|
+
align-items: center;
|
|
216
|
+
color: var(--at-color-deep);
|
|
217
|
+
font-size: 20px;
|
|
218
|
+
|
|
219
|
+
.badge-grp {
|
|
220
|
+
display: flex;
|
|
221
|
+
margin-left: 5px;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
.badge {
|
|
225
|
+
margin-left: 6px;
|
|
226
|
+
font-size: 13px;
|
|
227
|
+
color: var(--at-color-meta);
|
|
228
|
+
background: var(--at-color-bg-grey);
|
|
229
|
+
padding: 0 6px;
|
|
230
|
+
line-height: 17px;
|
|
231
|
+
border-radius: 2px;
|
|
232
|
+
color: #fff;
|
|
233
|
+
|
|
234
|
+
&.admin {
|
|
235
|
+
background: #0083ff;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
&.in-conf {
|
|
239
|
+
background: #89b1a5;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
.sub {
|
|
245
|
+
color: var(--at-color-sub);
|
|
246
|
+
font-size: 15px;
|
|
247
|
+
margin-top: 5px;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
.user-actions {
|
|
252
|
+
margin-top: 10px;
|
|
253
|
+
|
|
254
|
+
& > span {
|
|
255
|
+
cursor: pointer;
|
|
256
|
+
color: var(--at-color-meta);
|
|
257
|
+
font-size: 13px;
|
|
258
|
+
|
|
259
|
+
&:not(:last-child) {
|
|
260
|
+
margin-right: 16px;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
&:hover {
|
|
264
|
+
color: var(--at-color-deep);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
</style>
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { defineStore } from 'pinia'
|
|
2
|
+
import type { ArtalkType } from '@esershnr/artalk'
|
|
3
|
+
import { artalk, bootParams, getArtalk } from '@/global'
|
|
4
|
+
import { useMobileWidth } from '@/hooks/MobileWidth'
|
|
5
|
+
|
|
6
|
+
type TabsObj = { [name: string]: string }
|
|
7
|
+
|
|
8
|
+
export const useNavStore = defineStore('nav', () => {
|
|
9
|
+
const curtTab = ref('')
|
|
10
|
+
const tabs = ref<TabsObj>({})
|
|
11
|
+
|
|
12
|
+
const curtPage = ref('comments')
|
|
13
|
+
const sites = ref<ArtalkType.SiteData[]>([])
|
|
14
|
+
const siteSwitcherShow = ref(false)
|
|
15
|
+
|
|
16
|
+
const isSearchEnabled = ref(false)
|
|
17
|
+
const searchEvent = ref<((val: string) => void) | null>(null)
|
|
18
|
+
const searchResetEvent = ref<(() => void) | null>(null)
|
|
19
|
+
|
|
20
|
+
const isPageLoading = ref(false)
|
|
21
|
+
const scrollableArea = ref<HTMLElement | null>(null)
|
|
22
|
+
|
|
23
|
+
const darkMode = ref(bootParams.darkMode)
|
|
24
|
+
watch(darkMode, (val) => {
|
|
25
|
+
getArtalk()?.setDarkMode(val)
|
|
26
|
+
if (val != window.matchMedia('(prefers-color-scheme: dark)').matches)
|
|
27
|
+
localStorage.setItem('ATK_SIDEBAR_DARK_MODE', val ? '1' : '0')
|
|
28
|
+
else localStorage.removeItem('ATK_SIDEBAR_DARK_MODE') // enable auto switch
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
const updateTabs = (aTabs: TabsObj, activeTab?: string) => {
|
|
32
|
+
tabs.value = aTabs
|
|
33
|
+
if (activeTab) curtTab.value = activeTab
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const setTabActive = (tabName: string) => {
|
|
37
|
+
curtTab.value = tabName
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const showSiteSwitcher = () => {
|
|
41
|
+
siteSwitcherShow.value = true
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const hideSiteSwitcher = () => {
|
|
45
|
+
siteSwitcherShow.value = false
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const toggleSiteSwitcher = () => {
|
|
49
|
+
siteSwitcherShow.value = !siteSwitcherShow.value
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const scrollPageToTop = () => {
|
|
53
|
+
scrollableArea.value?.scrollTo(0, 0)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const scrollPageToEl = (el: HTMLElement) => {
|
|
57
|
+
scrollableArea.value?.scrollTo(0, el.offsetTop)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const setPageLoading = (pageLoading: boolean) => {
|
|
61
|
+
isPageLoading.value = pageLoading
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const refreshSites = () => {
|
|
65
|
+
artalk?.ctx
|
|
66
|
+
.getApi()
|
|
67
|
+
.sites.getSites()
|
|
68
|
+
.then((res) => {
|
|
69
|
+
sites.value = res.data.sites
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const enableSearch = (searchEvt: (val: string) => void, searchResetEvt: () => void) => {
|
|
74
|
+
isSearchEnabled.value = true
|
|
75
|
+
searchEvent.value = searchEvt
|
|
76
|
+
searchResetEvent.value = searchResetEvt
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const toggleDarkMode = () => {
|
|
80
|
+
darkMode.value = !darkMode.value
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
useRouter().beforeEach((to, from) => {
|
|
84
|
+
isSearchEnabled.value = false
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
const isMobile = useMobileWidth()
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
sites,
|
|
91
|
+
curtPage,
|
|
92
|
+
curtTab,
|
|
93
|
+
tabs,
|
|
94
|
+
siteSwitcherShow,
|
|
95
|
+
scrollableArea,
|
|
96
|
+
isPageLoading,
|
|
97
|
+
updateTabs,
|
|
98
|
+
setTabActive,
|
|
99
|
+
showSiteSwitcher,
|
|
100
|
+
hideSiteSwitcher,
|
|
101
|
+
toggleSiteSwitcher,
|
|
102
|
+
scrollPageToTop,
|
|
103
|
+
scrollPageToEl,
|
|
104
|
+
setPageLoading,
|
|
105
|
+
refreshSites,
|
|
106
|
+
isSearchEnabled,
|
|
107
|
+
searchEvent,
|
|
108
|
+
searchResetEvent,
|
|
109
|
+
enableSearch,
|
|
110
|
+
isMobile,
|
|
111
|
+
darkMode,
|
|
112
|
+
toggleDarkMode,
|
|
113
|
+
}
|
|
114
|
+
})
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { LocalUser } from '@esershnr/artalk'
|
|
2
|
+
import { defineStore } from 'pinia'
|
|
3
|
+
import sha256 from 'crypto-js/sha256'
|
|
4
|
+
import md5 from 'crypto-js/md5'
|
|
5
|
+
import { bootParams, getArtalk } from '../global'
|
|
6
|
+
|
|
7
|
+
interface UserState extends LocalUser {
|
|
8
|
+
/**
|
|
9
|
+
* Current site name
|
|
10
|
+
*/
|
|
11
|
+
site: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const useUserStore = defineStore('user', {
|
|
15
|
+
state: () =>
|
|
16
|
+
<UserState>{
|
|
17
|
+
site: bootParams.site || '',
|
|
18
|
+
...bootParams.user,
|
|
19
|
+
},
|
|
20
|
+
actions: {
|
|
21
|
+
logout() {
|
|
22
|
+
this.$reset()
|
|
23
|
+
getArtalk()?.ctx.getUser().logout()
|
|
24
|
+
},
|
|
25
|
+
sync() {
|
|
26
|
+
const user = getArtalk()?.ctx.getUser()
|
|
27
|
+
if (!user) throw new Error('Artalk is not initialized')
|
|
28
|
+
if (!user.checkHasBasicUserInfo()) throw new Error('User is not logged in')
|
|
29
|
+
this.$patch({ ...user.getData(), site: '' })
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
getters: {
|
|
33
|
+
avatar: (state) => getGravatar(state.email),
|
|
34
|
+
},
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
function getGravatar(email: string) {
|
|
38
|
+
// TODO get avatar url from backend api
|
|
39
|
+
const conf = getArtalk()?.ctx.conf?.gravatar
|
|
40
|
+
if (!conf) return ''
|
|
41
|
+
|
|
42
|
+
const emailHash =
|
|
43
|
+
typeof conf.params == 'string' && conf.params.includes('sha256=1')
|
|
44
|
+
? sha256(email.toLowerCase()).toString()
|
|
45
|
+
: md5(email.toLowerCase()).toString()
|
|
46
|
+
|
|
47
|
+
return `${conf.mirror.replace(/\/$/, '')}/${emailHash}?${conf.params.replace(/^\?/, '')}`
|
|
48
|
+
}
|