Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

perf(ui/sidebar): optimize setup process and i18n lazy load #962

Merged
merged 1 commit into from
Aug 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/docs/guide/frontend/i18n.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ locale: zh-CN
| -------- | -------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
| 后端程序 | [/i18n/[LANG].yml](https://github.com/ArtalkJS/Artalk/tree/master/i18n) | [zh-CN.yml](https://github.com/ArtalkJS/Artalk/blob/master/i18n/zh-CN.yml) |
| 前端界面 | [/ui/artalk/src/i18n/[LANG].ts](https://github.com/ArtalkJS/Artalk/tree/master/ui/artalk/src/i18n) | [zh-CN.ts](https://github.com/ArtalkJS/Artalk/blob/master/ui/artalk/src/i18n/zh-CN.ts) |
| 后台界面 | [/ui/artalk-sidebar/src/i18n/[LANG].ts](https://github.com/ArtalkJS/Artalk/blob/master/ui/artalk-sidebar/src/i18n) | [zh-CN.ts](https://github.com/ArtalkJS/Artalk/blob/master/ui/artalk-sidebar/src/i18n/zh-CN.ts) |
| 配置文件 | [/conf/artalk.example.[LANG].yml](https://github.com/ArtalkJS/Artalk/tree/master/conf) | [artalk.example.zh-CN.yml](https://github.com/ArtalkJS/Artalk/blob/master/conf/artalk.example.zh-CN.yml) |
| 说明文档 | [/docs/[LANG]/\*\*/\*.md](https://github.com/ArtalkJS/Artalk/tree/master/docs) | [/docs/\*\*/\*.md](https://github.com/ArtalkJS/Artalk/tree/master/docs) |

Expand Down
86 changes: 3 additions & 83 deletions ui/artalk-sidebar/src/App.vue
Original file line number Diff line number Diff line change
@@ -1,87 +1,11 @@
<script setup lang="ts">
import { storeToRefs } from 'pinia'
import type Artalk from 'artalk'
import { useNavStore } from './stores/nav'
import { useUserStore } from './stores/user'
import { getArtalk, bootParams } from './global'
import { bootParams } from './global'

const nav = useNavStore()
const user = useUserStore()
const route = useRoute()
const router = useRouter()
const { scrollableArea } = storeToRefs(nav)

const artalkLoaded = ref(false)

onBeforeMount(() => {
const artalk = getArtalk()
if (!artalk) {
throw new Error('Artalk instance not initialized')
}

artalk.on('mounted', () => {
if (artalkLoaded.value) return
artalkLoaded.value = true

syncArtalk(artalk)
})
})

function syncArtalk(artalk: Artalk) {
// access from open sidebar or directly by url
if (bootParams.user?.email) {
// sync user from sidebar to artalk
artalk.ctx.get('user').update(bootParams.user)
} else {
// sync user from artalk to sidebar
try {
useUserStore().sync()
} catch {
nextTick(() => {
router.replace('/login')
})
return
}
}

// 验证登录身份有效性
const artalkUser = artalk.ctx.get('user')
const artalkUserData = artalkUser.getData()

const logout = () => {
user.logout()
nextTick(() => {
router.replace('/login')
})
}

// Remove login failed dialog if sidebar
artalk.ctx.getApiHandlers().remove('need_login')
artalk.ctx.getApiHandlers().add('need_login', async () => {
logout()
throw new Error('Need login')
})

// Check user status
artalk.ctx
.getApi()
.user.getUserStatus({
email: artalkUserData.email,
name: artalkUserData.name,
})
.then((res) => {
if (res.data.is_admin && !res.data.is_login) {
logout()
} else {
// 将全部通知标记为已读
artalk.ctx.getApi().notifies.markAllNotifyRead({
email: artalkUserData.email,
name: artalkUserData.name,
})
}
})
}

const darkMode = ref(bootParams.darkMode)

;(function initDarkModeWatchMedia() {
Expand All @@ -94,11 +18,7 @@ const darkMode = ref(bootParams.darkMode)
</script>

<template>
<div
v-if="artalkLoaded"
class="app-wrap artalk atk-sidebar"
:class="{ 'atk-dark-mode': darkMode }"
>
<div class="app-wrap artalk atk-sidebar" :class="{ 'atk-dark-mode': darkMode }">
<AppHeader />
<AppNavigation />

Expand Down Expand Up @@ -137,7 +57,7 @@ $sidebarWidth: 280px;
}
}

// 分页条占位
// The placeholder area for the pagination bar
:deep(.atk-pagination-wrap) {
z-index: 200;
position: fixed;
Expand Down
84 changes: 84 additions & 0 deletions ui/artalk-sidebar/src/artalk.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import Artalk from 'artalk'
import { Router } from 'vue-router'
import { bootParams } from './global'
import { useUserStore } from './stores/user'

export function setupArtalk() {
// Create virtual element for Artalk
const artalkEl = document.createElement('div')
artalkEl.style.display = 'none'
document.body.append(artalkEl)

// Init Artalk
return Artalk.init({
el: artalkEl,
server: '../',
pageKey: bootParams.pageKey,
site: bootParams.site,
darkMode: bootParams.darkMode,
useBackendConf: true,
pvAdd: false,
remoteConfModifier: (conf) => {
conf.noComment = `<div class="atk-sidebar-no-content"></div>`
conf.flatMode = true
conf.pagination = {
pageSize: 20,
readMore: false,
autoLoad: false,
}
conf.listUnreadHighlight = true
},
})
}

export async function syncArtalkUser(artalk: Artalk, router: Router) {
const user = useUserStore()
const logout = () => {
user.logout()
nextTick(() => {
router.replace('/login')
})
}

// Access from open sidebar or directly by url
if (bootParams.user?.email) {
// sync user from sidebar to artalk
artalk.ctx.get('user').update(bootParams.user)
} else {
// Sync user from artalk to sidebar
try {
user.sync()
} catch {
logout()
return
}
}

// Async check user status (no await)
checkUser(artalk, logout)
}

function checkUser(artalk: Artalk, logout: () => void) {
// Get user info from artalk
const { name, email } = artalk.ctx.get('user').getData()

// Remove login failed dialog if sidebar
artalk.ctx.getApiHandlers().remove('need_login')
artalk.ctx.getApiHandlers().add('need_login', async () => {
logout()
throw new Error('Need login')
})

// Check user status
artalk.ctx
.getApi()
.user.getUserStatus({ email, name })
.then((res) => {
if (res.data.is_admin && !res.data.is_login) {
logout()
} else {
// Mark all notifications as read
artalk.ctx.getApi().notifies.markAllNotifyRead({ email, name })
}
})
}
32 changes: 5 additions & 27 deletions ui/artalk-sidebar/src/global.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@ export function getArtalk() {
return artalk
}

// 启动参数
/**
* Boot params from URL search params
*
* TODO: Refactor to a singleton store
*/
export const bootParams = getBootParams()

function getBootParams() {
Expand Down Expand Up @@ -43,32 +47,6 @@ function getBootParams() {
}
}

export function initArtalk() {
const artalkEl = document.createElement('div')
artalkEl.style.display = 'none'
document.body.append(artalkEl)

return Artalk.init({
el: artalkEl,
server: '../',
pageKey: bootParams.pageKey,
site: bootParams.site,
darkMode: bootParams.darkMode,
useBackendConf: true,
pvAdd: false,
remoteConfModifier: (conf) => {
conf.noComment = `<div class="atk-sidebar-no-content"></div>` // TODO i18n t('noComment')
conf.flatMode = true
conf.pagination = {
pageSize: 20,
readMore: false,
autoLoad: false,
}
conf.listUnreadHighlight = true
},
})
}

export function isOpenFromSidebar() {
return !!bootParams.user?.email
}
99 changes: 99 additions & 0 deletions ui/artalk-sidebar/src/i18n-en.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
export const en = {
ctrlCenter: 'Admin',
msgCenter: 'Messages',
noContent: 'No Content',
searchHint: 'Search by keywords...',
allSites: 'All Sites',
siteManage: 'Site Management',
comment: 'Comment',
page: 'Page',
user: 'User',
site: 'Site',
transfer: 'Transfer',
settings: 'Settings',
all: 'All',
pending: 'Pending',
personal: 'Personal',
mentions: 'Mentions',
mine: 'Mine',
admin: 'Admin',
create: 'Create',
import: 'Import',
export: 'Export',
settingSaved: 'Setting saved',
settingSaveFailed: 'Setting save failed',
settingNotice: 'Note: Some config options may require a manual reboot to take effect.',
apply: 'Apply',
updateComplete: 'Update complete',
updateReady: 'Ready to update...',
opFailed: 'Operation failed',
updateTitle: 'Fetch Title',
uploading: 'Uploading',
cancel: 'Cancel',
back: 'Back',
cacheClear: 'Cache Clear',
cacheWarm: 'Cache Warm',
editTitle: 'Edit Title',
switchKey: 'Switch Key',
commentAllowAll: 'Anyone Comment',
commentOnlyAdmin: 'Admin Comment Only',
config: 'Config',
envVarControlHint: 'Referenced by the environment variable {key}',
userAdminHint: 'Admin user',
userInConfHint: 'This user is defined in config file',
edit: 'Edit',
delete: 'Delete',
siteCount: 'Total {count} Sites',
createSite: 'Create Site',
siteName: 'Site Name',
siteUrls: 'Site URLs',
multiSepHint: 'multiple separated by commas',
add: 'Add',
rename: 'Rename',
inputHint: 'Input text...',
userCreate: 'Create User',
userEdit: 'Edit User',
userInConfCannotEditHint:
'Cannot edit user in config file, please modify the config file manually',
userDeleteConfirm:
'This operation will delete all comments of user: "{name}" email: "{email}", including the reply comments under his comments. Continue?',
userDeleteManuallyHint:
'User has been deleted from the database, please manually edit the config file and delete the user',
pageDeleteConfirm:
'This operation will delete the page: "{title}" and all data under it. Continue?',
siteDeleteConfirm:
'This operation will delete the site: "{name}" and all data under it. Continue?',
siteNameInputHint: 'Please enter the site name',
comments: 'Comments',
last: 'Last',
show: 'Show',
username: 'Username',
email: 'Email',
link: 'Link',
badgeText: 'Badge Text',
badgeColor: 'Badge Color',
role: 'Role',
normal: 'Normal',
password: 'Password',
passwordEmptyHint: 'leave blank not change your password',
emailNotify: 'Email notification',
enabled: 'Enabled',
disabled: 'Disabled',
save: 'Save',
dataFile: 'Data File',
artransfer: 'Artransfer',
targetSiteName: 'Target Site Name',
targetSiteURL: 'Target Site URL',
payload: 'Payload',
optional: 'Optional',
uploadReadyToImport: 'File uploaded and is ready for import',
artransferToolHint: 'Use the {link} to convert data to Artrans format.',
moreDetails: 'More details',
loginFailure: 'Login failure',
login: 'Login',
logout: 'Logout',
logoutConfirm: 'Are you sure you want to log out?',
loginSelectHint: 'Please select the account you wish to log into:',
}

export default en
37 changes: 37 additions & 0 deletions ui/artalk-sidebar/src/i18n.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { createI18n, type I18n, type Locale } from 'vue-i18n'
import { en } from './i18n-en'

export type MessageSchema = typeof en

export function setupI18n() {
const i18n = createI18n({
legacy: false, // use i18n in Composition API
locale: 'en',
fallbackLocale: 'en',
messages: { en } as any,
})

const setLocale = async (value: string) => {
await loadLocaleMessages(i18n, value)
i18n.global.locale.value = value
}

return { i18n, setLocale }
}

export async function loadLocaleMessages(i18n: I18n, locale: Locale) {
if (i18n.global.availableLocales.includes(locale)) return

// Load locale messages with dynamic import
// @see https://vitejs.dev/guide/features#dynamic-import
const messages = await import(`./i18n/${locale}.ts`)
.then((r: any) => r.default || r)
.catch(() => {
console.error(`Failed to load locale messages for "${locale}"`)
return
})

// Set locale and locale message
i18n.global.setLocaleMessage(locale, messages)
return nextTick()
}
Loading