Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ba8a221833 | ||
|
|
3f73cd9443 |
No files matched your search
@@ -1,14 +0,0 @@
|
||||
# don't ever lint node_modules
|
||||
node_modules
|
||||
# don't lint build output (make sure it's set to your correct build folder name)
|
||||
dist
|
||||
# don't lint nyc coverage output
|
||||
coverage
|
||||
|
||||
vite*.config*
|
||||
|
||||
# ignore all js files
|
||||
**/*.js
|
||||
|
||||
# ignore all config files
|
||||
*.config.ts
|
||||
+2
-1
@@ -16,7 +16,8 @@ module.exports = {
|
||||
'plugin:import/typescript',
|
||||
'plugin:compat/recommended',
|
||||
],
|
||||
plugins: ['@typescript-eslint', 'import'],
|
||||
plugins: ['prettier', '@typescript-eslint', 'import'],
|
||||
ignorePatterns: ['node_modules/', 'coverage/', 'dist/', '**/*.js'],
|
||||
rules: {
|
||||
'no-alert': 'warn',
|
||||
'no-unused-vars': 0,
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
.DS_Store
|
||||
node_modules/
|
||||
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
.idea
|
||||
.vscode
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
.history
|
||||
.editorconfig
|
||||
|
||||
# git
|
||||
.gitignore
|
||||
.gitattributes
|
||||
|
||||
# tests
|
||||
test
|
||||
coverage
|
||||
|
||||
# build tools
|
||||
.travis.yml
|
||||
.jenkins.yml
|
||||
.codeclimate.yml
|
||||
|
||||
# linters
|
||||
.jscsrc
|
||||
.jshintrc
|
||||
.eslintrc*
|
||||
|
||||
# vite configs
|
||||
vite*.config*
|
||||
|
||||
# develop tool configs
|
||||
.prettierrc
|
||||
.eslintrc.js
|
||||
.eslintignore
|
||||
postcss.config.js
|
||||
|
||||
yarn.lock
|
||||
package-lock.json
|
||||
|
||||
# local files
|
||||
deploy
|
||||
local
|
||||
CNAME
|
||||
index.html
|
||||
#dist
|
||||
|
||||
.github
|
||||
@@ -0,0 +1,11 @@
|
||||
# dist files
|
||||
dist/
|
||||
|
||||
# node modules
|
||||
node_modules/
|
||||
|
||||
# pnpm lock file
|
||||
pnpm-lock.yaml
|
||||
|
||||
# Minified files
|
||||
artalk-sidebar/src/lib/md5.js
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"semi": false,
|
||||
"arrowParens": "always",
|
||||
"singleQuote": true,
|
||||
"htmlWhitespaceSensitivity": "ignore"
|
||||
}
|
||||
+9
-2
@@ -7,7 +7,8 @@
|
||||
"dev:sidebar": "pnpm -F @artalk/artalk-sidebar dev",
|
||||
"build": "pnpm -F artalk build",
|
||||
"build:sidebar": "pnpm -F @artalk/artalk-sidebar build",
|
||||
"build:all": "pnpm build && pnpm build:sidebar"
|
||||
"build:all": "pnpm build && pnpm build:sidebar",
|
||||
"lint:prettier": "prettier --check --write ."
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.40.1",
|
||||
@@ -36,5 +37,11 @@
|
||||
},
|
||||
"browserslist": [
|
||||
"supports es6 and supports es6-class and supports es6-generators and supports es6-module-dynamic-import and supports es6-module and supports es6-number and supports es6-string-includes"
|
||||
]
|
||||
],
|
||||
"prettier": {
|
||||
"semi": false,
|
||||
"arrowParens": "always",
|
||||
"singleQuote": true,
|
||||
"htmlWhitespaceSensitivity": "ignore"
|
||||
}
|
||||
}
|
||||
+68
-60
@@ -5,68 +5,76 @@
|
||||
// Generated by unplugin-auto-import
|
||||
export {}
|
||||
declare global {
|
||||
const EffectScope: typeof import('vue')['EffectScope']
|
||||
const computed: typeof import('vue')['computed']
|
||||
const createApp: typeof import('vue')['createApp']
|
||||
const customRef: typeof import('vue')['customRef']
|
||||
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
|
||||
const defineComponent: typeof import('vue')['defineComponent']
|
||||
const defineLoader: typeof import('vue-router/auto')['defineLoader']
|
||||
const definePage: typeof import('unplugin-vue-router/runtime')['_definePage']
|
||||
const effectScope: typeof import('vue')['effectScope']
|
||||
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
|
||||
const getCurrentScope: typeof import('vue')['getCurrentScope']
|
||||
const h: typeof import('vue')['h']
|
||||
const inject: typeof import('vue')['inject']
|
||||
const isProxy: typeof import('vue')['isProxy']
|
||||
const isReactive: typeof import('vue')['isReactive']
|
||||
const isReadonly: typeof import('vue')['isReadonly']
|
||||
const isRef: typeof import('vue')['isRef']
|
||||
const markRaw: typeof import('vue')['markRaw']
|
||||
const nextTick: typeof import('vue')['nextTick']
|
||||
const onActivated: typeof import('vue')['onActivated']
|
||||
const onBeforeMount: typeof import('vue')['onBeforeMount']
|
||||
const onBeforeRouteLeave: typeof import('vue-router/auto')['onBeforeRouteLeave']
|
||||
const onBeforeRouteUpdate: typeof import('vue-router/auto')['onBeforeRouteUpdate']
|
||||
const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
|
||||
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
|
||||
const onDeactivated: typeof import('vue')['onDeactivated']
|
||||
const onErrorCaptured: typeof import('vue')['onErrorCaptured']
|
||||
const onMounted: typeof import('vue')['onMounted']
|
||||
const onRenderTracked: typeof import('vue')['onRenderTracked']
|
||||
const onRenderTriggered: typeof import('vue')['onRenderTriggered']
|
||||
const onScopeDispose: typeof import('vue')['onScopeDispose']
|
||||
const onServerPrefetch: typeof import('vue')['onServerPrefetch']
|
||||
const onUnmounted: typeof import('vue')['onUnmounted']
|
||||
const onUpdated: typeof import('vue')['onUpdated']
|
||||
const provide: typeof import('vue')['provide']
|
||||
const reactive: typeof import('vue')['reactive']
|
||||
const readonly: typeof import('vue')['readonly']
|
||||
const ref: typeof import('vue')['ref']
|
||||
const resolveComponent: typeof import('vue')['resolveComponent']
|
||||
const shallowReactive: typeof import('vue')['shallowReactive']
|
||||
const shallowReadonly: typeof import('vue')['shallowReadonly']
|
||||
const shallowRef: typeof import('vue')['shallowRef']
|
||||
const toRaw: typeof import('vue')['toRaw']
|
||||
const toRef: typeof import('vue')['toRef']
|
||||
const toRefs: typeof import('vue')['toRefs']
|
||||
const toValue: typeof import('vue')['toValue']
|
||||
const triggerRef: typeof import('vue')['triggerRef']
|
||||
const unref: typeof import('vue')['unref']
|
||||
const useAttrs: typeof import('vue')['useAttrs']
|
||||
const useCssModule: typeof import('vue')['useCssModule']
|
||||
const useCssVars: typeof import('vue')['useCssVars']
|
||||
const useI18n: typeof import('vue-i18n')['useI18n']
|
||||
const useRoute: typeof import('vue-router/auto')['useRoute']
|
||||
const useRouter: typeof import('vue-router/auto')['useRouter']
|
||||
const useSlots: typeof import('vue')['useSlots']
|
||||
const watch: typeof import('vue')['watch']
|
||||
const watchEffect: typeof import('vue')['watchEffect']
|
||||
const watchPostEffect: typeof import('vue')['watchPostEffect']
|
||||
const watchSyncEffect: typeof import('vue')['watchSyncEffect']
|
||||
const EffectScope: (typeof import('vue'))['EffectScope']
|
||||
const computed: (typeof import('vue'))['computed']
|
||||
const createApp: (typeof import('vue'))['createApp']
|
||||
const customRef: (typeof import('vue'))['customRef']
|
||||
const defineAsyncComponent: (typeof import('vue'))['defineAsyncComponent']
|
||||
const defineComponent: (typeof import('vue'))['defineComponent']
|
||||
const defineLoader: (typeof import('vue-router/auto'))['defineLoader']
|
||||
const definePage: (typeof import('unplugin-vue-router/runtime'))['_definePage']
|
||||
const effectScope: (typeof import('vue'))['effectScope']
|
||||
const getCurrentInstance: (typeof import('vue'))['getCurrentInstance']
|
||||
const getCurrentScope: (typeof import('vue'))['getCurrentScope']
|
||||
const h: (typeof import('vue'))['h']
|
||||
const inject: (typeof import('vue'))['inject']
|
||||
const isProxy: (typeof import('vue'))['isProxy']
|
||||
const isReactive: (typeof import('vue'))['isReactive']
|
||||
const isReadonly: (typeof import('vue'))['isReadonly']
|
||||
const isRef: (typeof import('vue'))['isRef']
|
||||
const markRaw: (typeof import('vue'))['markRaw']
|
||||
const nextTick: (typeof import('vue'))['nextTick']
|
||||
const onActivated: (typeof import('vue'))['onActivated']
|
||||
const onBeforeMount: (typeof import('vue'))['onBeforeMount']
|
||||
const onBeforeRouteLeave: (typeof import('vue-router/auto'))['onBeforeRouteLeave']
|
||||
const onBeforeRouteUpdate: (typeof import('vue-router/auto'))['onBeforeRouteUpdate']
|
||||
const onBeforeUnmount: (typeof import('vue'))['onBeforeUnmount']
|
||||
const onBeforeUpdate: (typeof import('vue'))['onBeforeUpdate']
|
||||
const onDeactivated: (typeof import('vue'))['onDeactivated']
|
||||
const onErrorCaptured: (typeof import('vue'))['onErrorCaptured']
|
||||
const onMounted: (typeof import('vue'))['onMounted']
|
||||
const onRenderTracked: (typeof import('vue'))['onRenderTracked']
|
||||
const onRenderTriggered: (typeof import('vue'))['onRenderTriggered']
|
||||
const onScopeDispose: (typeof import('vue'))['onScopeDispose']
|
||||
const onServerPrefetch: (typeof import('vue'))['onServerPrefetch']
|
||||
const onUnmounted: (typeof import('vue'))['onUnmounted']
|
||||
const onUpdated: (typeof import('vue'))['onUpdated']
|
||||
const provide: (typeof import('vue'))['provide']
|
||||
const reactive: (typeof import('vue'))['reactive']
|
||||
const readonly: (typeof import('vue'))['readonly']
|
||||
const ref: (typeof import('vue'))['ref']
|
||||
const resolveComponent: (typeof import('vue'))['resolveComponent']
|
||||
const shallowReactive: (typeof import('vue'))['shallowReactive']
|
||||
const shallowReadonly: (typeof import('vue'))['shallowReadonly']
|
||||
const shallowRef: (typeof import('vue'))['shallowRef']
|
||||
const toRaw: (typeof import('vue'))['toRaw']
|
||||
const toRef: (typeof import('vue'))['toRef']
|
||||
const toRefs: (typeof import('vue'))['toRefs']
|
||||
const toValue: (typeof import('vue'))['toValue']
|
||||
const triggerRef: (typeof import('vue'))['triggerRef']
|
||||
const unref: (typeof import('vue'))['unref']
|
||||
const useAttrs: (typeof import('vue'))['useAttrs']
|
||||
const useCssModule: (typeof import('vue'))['useCssModule']
|
||||
const useCssVars: (typeof import('vue'))['useCssVars']
|
||||
const useI18n: (typeof import('vue-i18n'))['useI18n']
|
||||
const useRoute: (typeof import('vue-router/auto'))['useRoute']
|
||||
const useRouter: (typeof import('vue-router/auto'))['useRouter']
|
||||
const useSlots: (typeof import('vue'))['useSlots']
|
||||
const watch: (typeof import('vue'))['watch']
|
||||
const watchEffect: (typeof import('vue'))['watchEffect']
|
||||
const watchPostEffect: (typeof import('vue'))['watchPostEffect']
|
||||
const watchSyncEffect: (typeof import('vue'))['watchSyncEffect']
|
||||
}
|
||||
// for type re-export
|
||||
declare global {
|
||||
// @ts-ignore
|
||||
export type { Component, ComponentPublicInstance, ComputedRef, InjectionKey, PropType, Ref, VNode } from 'vue'
|
||||
export type {
|
||||
Component,
|
||||
ComponentPublicInstance,
|
||||
ComputedRef,
|
||||
InjectionKey,
|
||||
PropType,
|
||||
Ref,
|
||||
VNode,
|
||||
} from 'vue'
|
||||
}
|
||||
+17
-17
@@ -7,22 +7,22 @@ export {}
|
||||
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
FileUploader: typeof import('./src/components/FileUploader.vue')['default']
|
||||
Header: typeof import('./src/components/Header.vue')['default']
|
||||
ItemTextEditor: typeof import('./src/components/ItemTextEditor.vue')['default']
|
||||
LoadingLayer: typeof import('./src/components/LoadingLayer.vue')['default']
|
||||
LogTerminal: typeof import('./src/components/LogTerminal.vue')['default']
|
||||
PageEditor: typeof import('./src/components/PageEditor.vue')['default']
|
||||
Pagination: typeof import('./src/components/Pagination.vue')['default']
|
||||
PreferenceArr: typeof import('./src/components/PreferenceArr.vue')['default']
|
||||
PreferenceGrp: typeof import('./src/components/PreferenceGrp.vue')['default']
|
||||
PreferenceItem: typeof import('./src/components/PreferenceItem.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
SiteCreate: typeof import('./src/components/SiteCreate.vue')['default']
|
||||
SiteEditor: typeof import('./src/components/SiteEditor.vue')['default']
|
||||
SiteSwitcher: typeof import('./src/components/SiteSwitcher.vue')['default']
|
||||
Tab: typeof import('./src/components/Tab.vue')['default']
|
||||
UserEditor: typeof import('./src/components/UserEditor.vue')['default']
|
||||
FileUploader: (typeof import('./src/components/FileUploader.vue'))['default']
|
||||
Header: (typeof import('./src/components/Header.vue'))['default']
|
||||
ItemTextEditor: (typeof import('./src/components/ItemTextEditor.vue'))['default']
|
||||
LoadingLayer: (typeof import('./src/components/LoadingLayer.vue'))['default']
|
||||
LogTerminal: (typeof import('./src/components/LogTerminal.vue'))['default']
|
||||
PageEditor: (typeof import('./src/components/PageEditor.vue'))['default']
|
||||
Pagination: (typeof import('./src/components/Pagination.vue'))['default']
|
||||
PreferenceArr: (typeof import('./src/components/PreferenceArr.vue'))['default']
|
||||
PreferenceGrp: (typeof import('./src/components/PreferenceGrp.vue'))['default']
|
||||
PreferenceItem: (typeof import('./src/components/PreferenceItem.vue'))['default']
|
||||
RouterLink: (typeof import('vue-router'))['RouterLink']
|
||||
RouterView: (typeof import('vue-router'))['RouterView']
|
||||
SiteCreate: (typeof import('./src/components/SiteCreate.vue'))['default']
|
||||
SiteEditor: (typeof import('./src/components/SiteEditor.vue'))['default']
|
||||
SiteSwitcher: (typeof import('./src/components/SiteSwitcher.vue'))['default']
|
||||
Tab: (typeof import('./src/components/Tab.vue'))['default']
|
||||
UserEditor: (typeof import('./src/components/UserEditor.vue'))['default']
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
|
||||
@@ -6,10 +6,19 @@ import { fileURLToPath } from 'url'
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = path.dirname(__filename)
|
||||
|
||||
fs.copyFile(path.join(__dirname, '../../../../artalk.example.yml'), path.join(__dirname, '../src/assets/artalk.example.yml'), (err) => {
|
||||
if (!err) console.log("\nArtalk config file 'artalk.example.yml' loaded.\n")
|
||||
else console.error("Failed to load config file 'artalk.example.yml':\n\n", err, "\n");
|
||||
})
|
||||
fs.copyFile(
|
||||
path.join(__dirname, '../../../../artalk.example.yml'),
|
||||
path.join(__dirname, '../src/assets/artalk.example.yml'),
|
||||
(err) => {
|
||||
if (!err) console.log("\nArtalk config file 'artalk.example.yml' loaded.\n")
|
||||
else
|
||||
console.error(
|
||||
"Failed to load config file 'artalk.example.yml':\n\n",
|
||||
err,
|
||||
'\n',
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
// const file = fs.createWriteStream()
|
||||
|
||||
|
||||
@@ -14,11 +14,15 @@ const i18n = useI18n()
|
||||
onBeforeMount(() => {
|
||||
// 获取语言
|
||||
if (!global.getBootParams().locale) {
|
||||
global.getArtalk().ctx.getApi().system.conf().then(resp => {
|
||||
if (resp.frontend_conf?.locale == 'string') {
|
||||
i18n.locale.value = resp.frontend_conf.locale
|
||||
}
|
||||
})
|
||||
global
|
||||
.getArtalk()
|
||||
.ctx.getApi()
|
||||
.system.conf()
|
||||
.then((resp) => {
|
||||
if (resp.frontend_conf?.locale == 'string') {
|
||||
i18n.locale.value = resp.frontend_conf.locale
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (bootParams.user?.email) {
|
||||
@@ -34,13 +38,17 @@ onBeforeMount(() => {
|
||||
}
|
||||
|
||||
// 验证登陆身份有效性
|
||||
global.getArtalk().ctx.getApi().user.loginStatus().then(resp => {
|
||||
if (resp.is_admin && !resp.is_login) {
|
||||
global.getArtalk().ctx.get('user').logout()
|
||||
user.logout()
|
||||
router.replace('/login')
|
||||
}
|
||||
})
|
||||
global
|
||||
.getArtalk()
|
||||
.ctx.getApi()
|
||||
.user.loginStatus()
|
||||
.then((resp) => {
|
||||
if (resp.is_admin && !resp.is_login) {
|
||||
global.getArtalk().ctx.get('user').logout()
|
||||
user.logout()
|
||||
router.replace('/login')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const darkMode = ref(bootParams.darkMode)
|
||||
@@ -55,7 +63,10 @@ const darkMode = ref(bootParams.darkMode)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="app-wrap artalk atk-sidebar" :class="{ 'atk-dark-mode': darkMode }">
|
||||
<div
|
||||
class="app-wrap artalk atk-sidebar"
|
||||
:class="{ 'atk-dark-mode': darkMode }"
|
||||
>
|
||||
<Header />
|
||||
<Tab />
|
||||
|
||||
|
||||
@@ -14,8 +14,8 @@ const emit = defineEmits<{
|
||||
|
||||
const { apiUrl } = toRefs(props)
|
||||
|
||||
let xhr: XMLHttpRequest|null = null
|
||||
const fileInputEl = ref<HTMLInputElement|null>(null)
|
||||
let xhr: XMLHttpRequest | null = null
|
||||
const fileInputEl = ref<HTMLInputElement | null>(null)
|
||||
const remoteFilename = ref('')
|
||||
const isUploading = ref(false)
|
||||
const progress = ref(0)
|
||||
@@ -45,7 +45,7 @@ async function startUploadFile(file: File) {
|
||||
const fileSize = file.size
|
||||
if (evt.loaded <= fileSize) {
|
||||
// 正在上传
|
||||
progress.value = Math.round(evt.loaded / fileSize * 100)
|
||||
progress.value = Math.round((evt.loaded / fileSize) * 100)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -56,7 +56,7 @@ async function startUploadFile(file: File) {
|
||||
|
||||
// 开始上传
|
||||
xhr.open('post', apiUrl.value)
|
||||
xhr.timeout = 5*60*1000 // 5分钟超时
|
||||
xhr.timeout = 5 * 60 * 1000 // 5分钟超时
|
||||
xhr.send(formData)
|
||||
|
||||
// 上传成功事件
|
||||
@@ -114,7 +114,13 @@ function abortUpload() {
|
||||
<template>
|
||||
<div class="atk-file-upload-group">
|
||||
<div v-show="!isUploading" class="atk-file-input-wrap atk-fade-in">
|
||||
<input ref="fileInputEl" type="file" name="AtkDataFile" accept=".artrans" @change="onFileInputChange()" />
|
||||
<input
|
||||
ref="fileInputEl"
|
||||
type="file"
|
||||
name="AtkDataFile"
|
||||
accept=".artrans"
|
||||
@change="onFileInputChange()"
|
||||
/>
|
||||
<div class="atk-desc">
|
||||
<slot v-if="!isDone" name="tip"></slot>
|
||||
<slot v-if="isDone" name="done-msg"></slot>
|
||||
@@ -134,6 +140,4 @@ function abortUpload() {
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
</style>
|
||||
<style scoped lang="scss"></style>
|
||||
@@ -14,7 +14,9 @@ const { site: curtSite, isAdmin, email } = storeToRefs(user)
|
||||
const userAvatarImgURL = computed(() => {
|
||||
const conf = artalk?.ctx.conf?.gravatar
|
||||
if (!conf) return ``
|
||||
return `${conf.mirror.replace(/\/$/, '')}/${MD5(email.value)}?${conf.params.replace(/^\?/, '')}`
|
||||
return `${conf.mirror.replace(/\/$/, '')}/${MD5(
|
||||
email.value,
|
||||
)}?${conf.params.replace(/^\?/, '')}`
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -23,7 +25,7 @@ const userAvatarImgURL = computed(() => {
|
||||
<template v-if="isAdmin">
|
||||
<div
|
||||
class="avatar clickable"
|
||||
:class="{ 'active': nav.siteSwitcherShow }"
|
||||
:class="{ active: nav.siteSwitcherShow }"
|
||||
@click="nav.showSiteSwitcher()"
|
||||
>
|
||||
<div class="site">{{ curtSite.substring(0, 1) }}</div>
|
||||
@@ -31,7 +33,7 @@ const userAvatarImgURL = computed(() => {
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="avatar">
|
||||
<img :src="userAvatarImgURL">
|
||||
<img :src="userAvatarImgURL" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -75,7 +77,8 @@ const userAvatarImgURL = computed(() => {
|
||||
&.clickable {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover, &.active {
|
||||
&:hover,
|
||||
&.active {
|
||||
background: var(--at-color-bg-grey);
|
||||
border-right: 1px solid var(--at-color-border);
|
||||
}
|
||||
@@ -100,7 +103,7 @@ const userAvatarImgURL = computed(() => {
|
||||
text-align: center;
|
||||
line-height: 30px;
|
||||
font-size: 13px;
|
||||
color: #FFF;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,14 +119,14 @@ const userAvatarImgURL = computed(() => {
|
||||
font-size: 20px;
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 6px;
|
||||
background: #0083ff;
|
||||
opacity: .4;
|
||||
opacity: 0.4;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,12 +6,12 @@ const props = defineProps<{
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(evt: 'yes', value: string): boolean|void|Promise<boolean|void>
|
||||
(evt: 'no', value: string): boolean|void|Promise<boolean|void>
|
||||
(evt: 'yes', value: string): boolean | void | Promise<boolean | void>
|
||||
(evt: 'no', value: string): boolean | void | Promise<boolean | void>
|
||||
(evt: 'close'): void
|
||||
}>()
|
||||
|
||||
const inputEl = ref<HTMLInputElement|null>(null)
|
||||
const inputEl = ref<HTMLInputElement | null>(null)
|
||||
const inputVal = ref('')
|
||||
const inputInvalid = ref(false)
|
||||
|
||||
@@ -30,13 +30,14 @@ function onInput() {
|
||||
}
|
||||
|
||||
function onKeyUp(evt: KeyboardEvent) {
|
||||
if (evt.key === 'Enter' || evt.keyCode === 13) { // 按下回车键
|
||||
if (evt.key === 'Enter' || evt.keyCode === 13) {
|
||||
// 按下回车键
|
||||
evt.preventDefault()
|
||||
submit('yes')
|
||||
}
|
||||
}
|
||||
|
||||
async function submit(type: 'yes'|'no') {
|
||||
async function submit(type: 'yes' | 'no') {
|
||||
if (type == 'yes' && inputInvalid.value) return
|
||||
|
||||
let isContinue: any = undefined
|
||||
@@ -67,7 +68,7 @@ async function submit(type: 'yes'|'no') {
|
||||
@input="onInput()"
|
||||
@keyup="onKeyUp"
|
||||
:class="{ 'atk-invalid': inputInvalid }"
|
||||
>
|
||||
/>
|
||||
</div>
|
||||
<div class="atk-actions">
|
||||
<div
|
||||
@@ -77,10 +78,7 @@ async function submit(type: 'yes'|'no') {
|
||||
>
|
||||
<i class="atk-icon atk-icon-yes"></i>
|
||||
</div>
|
||||
<div
|
||||
class="atk-item atk-no-btn"
|
||||
@click="submit('no')"
|
||||
>
|
||||
<div class="atk-item atk-no-btn" @click="submit('no')">
|
||||
<i class="atk-icon atk-icon-no"></i>
|
||||
</div>
|
||||
</div>
|
||||
@@ -113,7 +111,8 @@ async function submit(type: 'yes'|'no') {
|
||||
outline: none;
|
||||
background: transparent;
|
||||
|
||||
&.atk-invalid {}
|
||||
&.atk-invalid {
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border-bottom-color: var(--at-color-main);
|
||||
@@ -124,7 +123,8 @@ async function submit(type: 'yes'|'no') {
|
||||
.atk-actions {
|
||||
@extend .atk-list-btn-actions;
|
||||
|
||||
.atk-yes-btn.atk-disabled {}
|
||||
.atk-yes-btn.atk-disabled {
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -5,7 +5,7 @@ const props = defineProps<{
|
||||
}>()
|
||||
|
||||
const showSpinner = ref(false)
|
||||
let timer: number|undefined
|
||||
let timer: number | undefined
|
||||
|
||||
onMounted(() => {
|
||||
// spinner 延迟显示,若加载等待时间太短,没必要显示(闪一下即可)
|
||||
@@ -26,7 +26,16 @@ onUnmounted(() => {
|
||||
:style="{ background: props.transparentBg ? 'transparent' : undefined }"
|
||||
>
|
||||
<div v-if="showSpinner" class="atk-loading-spinner">
|
||||
<svg viewBox="25 25 50 50"><circle cx="50" cy="50" r="20" fill="none" stroke-width="2" stroke-miterlimit="10"></circle></svg>
|
||||
<svg viewBox="25 25 50 50">
|
||||
<circle
|
||||
cx="50"
|
||||
cy="50"
|
||||
r="20"
|
||||
fill="none"
|
||||
stroke-width="2"
|
||||
stroke-miterlimit="10"
|
||||
></circle>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -8,7 +8,7 @@ const emit = defineEmits<{
|
||||
(evt: 'back'): void
|
||||
}>()
|
||||
|
||||
const logWrapEl = ref<HTMLElement|null>(null)
|
||||
const logWrapEl = ref<HTMLElement | null>(null)
|
||||
|
||||
onMounted(() => {
|
||||
// 创建 iframe
|
||||
@@ -64,7 +64,8 @@ function back() {
|
||||
}
|
||||
}
|
||||
|
||||
.atk-log {}
|
||||
.atk-log {
|
||||
}
|
||||
|
||||
.atk-iframe {
|
||||
width: 100%;
|
||||
|
||||
@@ -13,8 +13,10 @@ const emit = defineEmits<{
|
||||
}>()
|
||||
|
||||
const { page } = toRefs(props)
|
||||
const editFieldKey = ref<keyof ArtalkType.PageData|null>(null)
|
||||
const editFieldVal = computed(() => String(editFieldKey ? page.value[editFieldKey.value!] || '' : ''))
|
||||
const editFieldKey = ref<keyof ArtalkType.PageData | null>(null)
|
||||
const editFieldVal = computed(() =>
|
||||
String(editFieldKey ? page.value[editFieldKey.value!] || '' : ''),
|
||||
)
|
||||
const isLoading = ref(false)
|
||||
const { t } = useI18n()
|
||||
|
||||
@@ -30,12 +32,16 @@ async function editAdminOnly() {
|
||||
isLoading.value = true
|
||||
let p: ArtalkType.PageData
|
||||
try {
|
||||
p = await artalk!.ctx.getApi().page.pageEdit({ ...page.value, admin_only: !page.value.admin_only })
|
||||
p = await artalk!.ctx
|
||||
.getApi()
|
||||
.page.pageEdit({ ...page.value, admin_only: !page.value.admin_only })
|
||||
} catch (err: any) {
|
||||
alert(`修改失败:${err.msg || '未知错误'}`)
|
||||
console.log(err)
|
||||
return
|
||||
} finally { isLoading.value = false }
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
emit('update', p)
|
||||
}
|
||||
|
||||
@@ -48,7 +54,9 @@ async function sync() {
|
||||
alert(`同步失败:${err.msg || '未知错误'}`)
|
||||
console.log(err)
|
||||
return
|
||||
} finally { isLoading.value = false }
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
emit('update', p)
|
||||
}
|
||||
|
||||
@@ -56,17 +64,26 @@ function del() {
|
||||
const del = async () => {
|
||||
isLoading.value = true
|
||||
try {
|
||||
await artalk!.ctx.getApi().page.pageDel(page.value.key, page.value.site_name)
|
||||
await artalk!.ctx
|
||||
.getApi()
|
||||
.page.pageDel(page.value.key, page.value.site_name)
|
||||
} catch (err: any) {
|
||||
console.log(err)
|
||||
alert(`删除失败 ${String(err)}`)
|
||||
return
|
||||
} finally { isLoading.value = false }
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
emit('remove', page.value.id)
|
||||
}
|
||||
if (window.confirm(
|
||||
`确认删除页面 "${page.value.title || page.value.key}"?将会删除所有相关数据`
|
||||
)) del()
|
||||
if (
|
||||
window.confirm(
|
||||
`确认删除页面 "${
|
||||
page.value.title || page.value.key
|
||||
}"?将会删除所有相关数据`,
|
||||
)
|
||||
)
|
||||
del()
|
||||
}
|
||||
|
||||
function close() {
|
||||
@@ -78,12 +95,16 @@ async function onFieldEditorYes(val: string) {
|
||||
isLoading.value = true
|
||||
let p: ArtalkType.PageData
|
||||
try {
|
||||
p = await artalk!.ctx.getApi().page.pageEdit({ ...page.value, [editFieldKey.value as any]: val })
|
||||
p = await artalk!.ctx
|
||||
.getApi()
|
||||
.page.pageEdit({ ...page.value, [editFieldKey.value as any]: val })
|
||||
} catch (err: any) {
|
||||
alert(`修改失败:${err.msg || '未知错误'}`)
|
||||
console.error(err)
|
||||
return false
|
||||
} finally { isLoading.value = false }
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
emit('update', p)
|
||||
}
|
||||
|
||||
@@ -100,13 +121,19 @@ function onFiledEditorNo() {
|
||||
<template>
|
||||
<div class="atk-page-edit-layer">
|
||||
<div class="atk-page-main-actions">
|
||||
<div class="atk-item atk-title-edit-btn" @click="editTitle()">{{ t('editTitle') }}</div>
|
||||
<div class="atk-item atk-key-edit-btn" @click="editKey()">{{ t('switchKey') }}</div>
|
||||
<div class="atk-item atk-title-edit-btn" @click="editTitle()">
|
||||
{{ t('editTitle') }}
|
||||
</div>
|
||||
<div class="atk-item atk-key-edit-btn" @click="editKey()">
|
||||
{{ t('switchKey') }}
|
||||
</div>
|
||||
<div
|
||||
class="atk-item atk-admin-only-btn"
|
||||
:class="!page.admin_only ? 'atk-green' : 'atk-yellow'"
|
||||
@click="editAdminOnly()"
|
||||
>{{ !page.admin_only ? t('commentAllowAll') : t('commentOnlyAdmin') }}</div>
|
||||
>
|
||||
{{ !page.admin_only ? t('commentAllowAll') : t('commentOnlyAdmin') }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="atk-page-actions">
|
||||
<div class="atk-item atk-sync-btn" @click="sync()">
|
||||
|
||||
@@ -20,11 +20,11 @@ const emit = defineEmits<{
|
||||
const curtPage = ref(1)
|
||||
const offset = computed(() => pageSize.value * (curtPage.value - 1))
|
||||
const maxPage = computed(() => Math.ceil(total.value / pageSize.value))
|
||||
const prevDisabled = computed(() => (curtPage.value - 1 < 1))
|
||||
const nextDisabled = computed(() => (curtPage.value + 1 > maxPage.value))
|
||||
const prevDisabled = computed(() => curtPage.value - 1 < 1)
|
||||
const nextDisabled = computed(() => curtPage.value + 1 > maxPage.value)
|
||||
|
||||
const inputValue = ref(String(curtPage.value))
|
||||
let inputTimer: number|undefined
|
||||
let inputTimer: number | undefined
|
||||
|
||||
function changePage(page: number) {
|
||||
curtPage.value = page
|
||||
@@ -36,14 +36,18 @@ function changePage(page: number) {
|
||||
function prev() {
|
||||
if (disabled?.value) return
|
||||
const page = curtPage.value - 1
|
||||
if (page < 1) { return }
|
||||
if (page < 1) {
|
||||
return
|
||||
}
|
||||
changePage(page)
|
||||
}
|
||||
|
||||
function next() {
|
||||
if (disabled?.value) return
|
||||
const page = curtPage.value + 1
|
||||
if (page > maxPage.value) { return }
|
||||
if (page > maxPage.value) {
|
||||
return
|
||||
}
|
||||
changePage(page)
|
||||
}
|
||||
|
||||
@@ -67,11 +71,22 @@ function triggerInput(now: boolean = false) {
|
||||
const value = inputValue.value.trim()
|
||||
|
||||
const modify = () => {
|
||||
if (value === '') { revokeInput();return }
|
||||
if (value === '') {
|
||||
revokeInput()
|
||||
return
|
||||
}
|
||||
let page = Number(value)
|
||||
if (Number.isNaN(page)) { revokeInput();return }
|
||||
if (page < 1) { revokeInput();return }
|
||||
if (page > maxPage.value) { page = maxPage.value }
|
||||
if (Number.isNaN(page)) {
|
||||
revokeInput()
|
||||
return
|
||||
}
|
||||
if (page < 1) {
|
||||
revokeInput()
|
||||
return
|
||||
}
|
||||
if (page > maxPage.value) {
|
||||
page = maxPage.value
|
||||
}
|
||||
changePage(page)
|
||||
}
|
||||
|
||||
@@ -86,13 +101,17 @@ function onInputKeydown(evt: KeyboardEvent) {
|
||||
if (keyCode === 38) {
|
||||
// 上键
|
||||
const page = Number(inputValue.value) + 1
|
||||
if (page > maxPage.value) { return }
|
||||
fillInput(page);
|
||||
if (page > maxPage.value) {
|
||||
return
|
||||
}
|
||||
fillInput(page)
|
||||
triggerInput(false)
|
||||
} else if (keyCode === 40) {
|
||||
// 下键
|
||||
const page = Number(inputValue.value) - 1
|
||||
if (page < 1) { return }
|
||||
if (page < 1) {
|
||||
return
|
||||
}
|
||||
fillInput(page)
|
||||
triggerInput(false)
|
||||
} else if (keyCode === 13) {
|
||||
@@ -111,7 +130,9 @@ defineExpose({ prev, next, reset })
|
||||
class="atk-btn atk-btn-prev"
|
||||
:class="{ 'atk-disabled': disabled || prevDisabled }"
|
||||
@click="prev()"
|
||||
>Prev</div>
|
||||
>
|
||||
Prev
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
class="atk-input"
|
||||
@@ -125,7 +146,9 @@ defineExpose({ prev, next, reset })
|
||||
class="atk-btn atk-btn-next"
|
||||
:class="{ 'atk-disabled': disabled || nextDisabled }"
|
||||
@click="next()"
|
||||
>Next</div>
|
||||
>
|
||||
Next
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
import settings from '../lib/settings'
|
||||
|
||||
const props = defineProps<{
|
||||
tplData: Array<any>,
|
||||
path: (string|number)[]
|
||||
tplData: Array<any>
|
||||
path: (string | number)[]
|
||||
}>()
|
||||
|
||||
const ci = getCurrentInstance()
|
||||
@@ -14,7 +14,8 @@ onMounted(() => {
|
||||
})
|
||||
|
||||
function update() {
|
||||
customValue.value = (settings.get().customs.value?.getIn(props.path) as any)?.items || []
|
||||
customValue.value =
|
||||
(settings.get().customs.value?.getIn(props.path) as any)?.items || []
|
||||
}
|
||||
|
||||
function save() {
|
||||
|
||||
@@ -4,11 +4,13 @@ import settings from '../lib/settings'
|
||||
const router = useRouter()
|
||||
|
||||
const props = defineProps<{
|
||||
tplData: Object|Array<any>
|
||||
path: (string|number)[]
|
||||
tplData: Object | Array<any>
|
||||
path: (string | number)[]
|
||||
}>()
|
||||
|
||||
const desc = computed(() => settings.get().extractItemDescFromComment(props.path))
|
||||
const desc = computed(() =>
|
||||
settings.get().extractItemDescFromComment(props.path),
|
||||
)
|
||||
const level = computed(() => props.path.length)
|
||||
|
||||
const expanded = ref(true)
|
||||
@@ -53,11 +55,7 @@ function onHeadClick(evt: Event) {
|
||||
:path="[...path, key]"
|
||||
/>
|
||||
<!-- Item Input -->
|
||||
<PreferenceItem
|
||||
v-else
|
||||
:tpl-data="value"
|
||||
:path="[...path, key]"
|
||||
/>
|
||||
<PreferenceItem v-else :tpl-data="value" :path="[...path, key]" />
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
@@ -87,7 +85,7 @@ function onHeadClick(evt: Event) {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
transition: height ease .2s;
|
||||
transition: height ease 0.2s;
|
||||
left: -10px;
|
||||
content: '';
|
||||
height: 10px;
|
||||
@@ -118,7 +116,8 @@ function onHeadClick(evt: Event) {
|
||||
}
|
||||
}
|
||||
|
||||
.pf-grp.level-1, .pf-grp.level-2 {
|
||||
.pf-grp.level-1,
|
||||
.pf-grp.level-2 {
|
||||
& > .pf-head > .sub-title {
|
||||
padding: 0 10px 0 10px;
|
||||
margin-left: 4px;
|
||||
@@ -127,7 +126,6 @@ function onHeadClick(evt: Event) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.pf-grp.level-3 {
|
||||
margin-left: 10px;
|
||||
|
||||
|
||||
@@ -3,23 +3,27 @@ import settings from '../lib/settings'
|
||||
|
||||
const props = defineProps<{
|
||||
tplData: any
|
||||
path: (string|number)[]
|
||||
path: (string | number)[]
|
||||
}>()
|
||||
|
||||
const desc = computed(() => settings.get().extractItemDescFromComment(props.path))
|
||||
const customValue = computed(() => settings.get().customs.value?.getIn(props.path) as any)
|
||||
const desc = computed(() =>
|
||||
settings.get().extractItemDescFromComment(props.path),
|
||||
)
|
||||
const customValue = computed(
|
||||
() => settings.get().customs.value?.getIn(props.path) as any,
|
||||
)
|
||||
|
||||
function onChange(value: boolean|string|number) {
|
||||
function onChange(value: boolean | string | number) {
|
||||
// 类型转换
|
||||
switch (typeof props.tplData) {
|
||||
case "boolean":
|
||||
if (value === "true") value = true
|
||||
else if (value === "false") value = false
|
||||
case 'boolean':
|
||||
if (value === 'true') value = true
|
||||
else if (value === 'false') value = false
|
||||
break
|
||||
case "string":
|
||||
case 'string':
|
||||
value = String(value)
|
||||
break
|
||||
case "number":
|
||||
case 'number':
|
||||
if (!isNaN(Number(value))) value = Number(value)
|
||||
break
|
||||
}
|
||||
@@ -32,34 +36,42 @@ function onChange(value: boolean|string|number) {
|
||||
<div class="pf-item">
|
||||
<div class="info">
|
||||
<div class="title">{{ desc.title }}</div>
|
||||
<div v-if="!!desc.subTitle" class="sub-title">{{ desc.subTitle }}</div>
|
||||
</div>
|
||||
<div v-if="!!desc.subTitle" class="sub-title">{{ desc.subTitle }}</div>
|
||||
</div>
|
||||
|
||||
<div class="value">
|
||||
<!-- 候选框 -->
|
||||
<template v-if="desc.opts !== null">
|
||||
<select :value="customValue" @change="onChange(($event.target as any).value)">
|
||||
<option
|
||||
v-for="item in desc.opts"
|
||||
:value="item"
|
||||
>{{ item }}</option>
|
||||
</select>
|
||||
</template>
|
||||
<div class="value">
|
||||
<!-- 候选框 -->
|
||||
<template v-if="desc.opts !== null">
|
||||
<select
|
||||
:value="customValue"
|
||||
@change="onChange(($event.target as any).value)"
|
||||
>
|
||||
<option v-for="item in desc.opts" :value="item">{{ item }}</option>
|
||||
</select>
|
||||
</template>
|
||||
|
||||
<!-- 开关 -->
|
||||
<template v-else-if="typeof tplData === 'boolean'">
|
||||
<input type="checkbox" :checked="customValue" @change="onChange(($event.target as any).checked)">
|
||||
</template>
|
||||
<!-- 开关 -->
|
||||
<template v-else-if="typeof tplData === 'boolean'">
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="customValue"
|
||||
@change="onChange(($event.target as any).checked)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- 文本框 -->
|
||||
<template v-else>
|
||||
<input
|
||||
type="text"
|
||||
:value="(typeof customValue === 'undefined' || customValue === null) ? '' : String(customValue)"
|
||||
@change="onChange(($event.target as any).value)"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
<!-- 文本框 -->
|
||||
<template v-else>
|
||||
<input
|
||||
type="text"
|
||||
:value="
|
||||
typeof customValue === 'undefined' || customValue === null
|
||||
? ''
|
||||
: String(customValue)
|
||||
"
|
||||
@change="onChange(($event.target as any).value)"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import type { ArtalkType } from 'artalk'
|
||||
const { t } = useI18n()
|
||||
|
||||
const props = defineProps<{
|
||||
initVal?: { name: string, urls: string }
|
||||
initVal?: { name: string; urls: string }
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -14,9 +14,9 @@ const emit = defineEmits<{
|
||||
}>()
|
||||
|
||||
const isLoading = ref(false)
|
||||
const site = ref<{name: string, urls: string}>({
|
||||
const site = ref<{ name: string; urls: string }>({
|
||||
name: '',
|
||||
urls: ''
|
||||
urls: '',
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
@@ -28,7 +28,10 @@ async function submit() {
|
||||
const siteName = site.value.name.trim()
|
||||
const siteUrls = site.value.urls.trim()
|
||||
|
||||
if (siteName === '') { alert('请输入站点名称'); return }
|
||||
if (siteName === '') {
|
||||
alert('请输入站点名称')
|
||||
return
|
||||
}
|
||||
|
||||
isLoading.value = true
|
||||
let s: ArtalkType.SiteData
|
||||
@@ -38,7 +41,9 @@ async function submit() {
|
||||
window.alert(`创建失败:${err.msg || ''}`)
|
||||
console.error(err)
|
||||
return
|
||||
} finally { isLoading.value = false }
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
|
||||
emit('done', s)
|
||||
}
|
||||
@@ -57,14 +62,26 @@ function close() {
|
||||
</div>
|
||||
</div>
|
||||
<form class="atk-form" @submit.prevent="submit()">
|
||||
<input v-model="site.name" type="text" name="AtkSiteName" :placeholder="t('siteName')" autocomplete="off">
|
||||
<input v-model="site.urls" type="text" name="AtkSiteUrls" :placeholder="`${t('siteUrls')} (${t('multiSepHint')})`" autocomplete="off">
|
||||
<button type="submit" class="atk-btn" name="AtkSubmit">{{ t('add') }}</button>
|
||||
<input
|
||||
v-model="site.name"
|
||||
type="text"
|
||||
name="AtkSiteName"
|
||||
:placeholder="t('siteName')"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<input
|
||||
v-model="site.urls"
|
||||
type="text"
|
||||
name="AtkSiteUrls"
|
||||
:placeholder="`${t('siteUrls')} (${t('multiSepHint')})`"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<button type="submit" class="atk-btn" name="AtkSubmit">
|
||||
{{ t('add') }}
|
||||
</button>
|
||||
</form>
|
||||
<LoadingLayer v-if="isLoading" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
</style>
|
||||
<style scoped lang="scss"></style>
|
||||
@@ -14,7 +14,7 @@ const emit = defineEmits<{
|
||||
|
||||
const { site } = toRefs(props)
|
||||
const isLoading = ref(false)
|
||||
const editFieldKey = ref<keyof ArtalkType.SiteData|null>(null)
|
||||
const editFieldKey = ref<keyof ArtalkType.SiteData | null>(null)
|
||||
const editFieldVal = computed(() => {
|
||||
if (editFieldKey.value === 'urls') return site.value.urls_raw || ''
|
||||
return String(editFieldKey ? site.value[editFieldKey.value!] || '' : '')
|
||||
@@ -47,12 +47,13 @@ function del() {
|
||||
console.log(err)
|
||||
alert(`删除失败 ${String(err)}`)
|
||||
return
|
||||
} finally { isLoading.value = false }
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
emit('remove', site.value.id)
|
||||
}
|
||||
if (window.confirm(
|
||||
`确认删除站点 "${site.value.name}"?将会删除所有相关数据`
|
||||
)) del()
|
||||
if (window.confirm(`确认删除站点 "${site.value.name}"?将会删除所有相关数据`))
|
||||
del()
|
||||
}
|
||||
|
||||
async function onFieldEditorYes(val: string) {
|
||||
@@ -60,12 +61,16 @@ async function onFieldEditorYes(val: string) {
|
||||
isLoading.value = true
|
||||
let s: ArtalkType.SiteData
|
||||
try {
|
||||
s = await artalk!.ctx.getApi().site.siteEdit({ ...site.value, [editFieldKey.value as any]: val })
|
||||
s = await artalk!.ctx
|
||||
.getApi()
|
||||
.site.siteEdit({ ...site.value, [editFieldKey.value as any]: val })
|
||||
} catch (err: any) {
|
||||
alert(`修改失败:${err.msg || '未知错误'}`)
|
||||
console.error(err)
|
||||
return false
|
||||
} finally { isLoading.value = false }
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
emit('update', s)
|
||||
}
|
||||
|
||||
@@ -85,13 +90,17 @@ function onFiledEditorNo() {
|
||||
<span
|
||||
class="atk-site-name"
|
||||
@click="!site.first_url || openURL(site.first_url)"
|
||||
>{{ site.name }}</span>
|
||||
>
|
||||
{{ site.name }}
|
||||
</span>
|
||||
<span class="atk-site-urls">
|
||||
<div
|
||||
v-for="(url) in site.urls"
|
||||
v-for="url in site.urls"
|
||||
class="atk-url-item"
|
||||
@click="openURL(url)"
|
||||
>{{ url }}</div>
|
||||
>
|
||||
{{ url }}
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
<div class="atk-close-btn" @click="close()">
|
||||
@@ -100,8 +109,12 @@ function onFiledEditorNo() {
|
||||
</div>
|
||||
<div class="atk-main">
|
||||
<div class="atk-site-text-actions">
|
||||
<div class="atk-item atk-rename-btn" @click="rename()">{{ t('rename') }}</div>
|
||||
<div class="atk-item atk-edit-url-btn" @click="editURL()">{{ t('edit') }} URL</div>
|
||||
<div class="atk-item atk-rename-btn" @click="rename()">
|
||||
{{ t('rename') }}
|
||||
</div>
|
||||
<div class="atk-item atk-edit-url-btn" @click="editURL()">
|
||||
{{ t('edit') }} URL
|
||||
</div>
|
||||
<!--<div class="atk-item atk-export-btn">导出</div>
|
||||
<div class="atk-item atk-import-btn">导入</div>-->
|
||||
</div>
|
||||
@@ -121,6 +134,4 @@ function onFiledEditorNo() {
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
</style>
|
||||
<style scoped lang="scss"></style>
|
||||
@@ -1,9 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useNavStore } from '../stores/nav'
|
||||
import { useUserStore } from '../stores/user'
|
||||
|
||||
const el = ref<HTMLElement|null>(null)
|
||||
const el = ref<HTMLElement | null>(null)
|
||||
|
||||
const nav = useNavStore()
|
||||
const { siteSwitcherShow: curtShow, sites } = storeToRefs(nav)
|
||||
@@ -37,11 +37,16 @@ const displaySites = computed(() => {
|
||||
displays.push({ label: t('allSites'), name: '__ATK_SITE_ALL', logoText: '_' })
|
||||
sites.value.forEach((site) => {
|
||||
displays.push({
|
||||
label: site.name, name: site.name,
|
||||
logoText: site.name.substring(0, 1)
|
||||
label: site.name,
|
||||
name: site.name,
|
||||
logoText: site.name.substring(0, 1),
|
||||
})
|
||||
})
|
||||
displays.push({ label: t('siteManage'), name: '__SITE_MANAGEMENT__', logoText: '+' })
|
||||
displays.push({
|
||||
label: t('siteManage'),
|
||||
name: '__SITE_MANAGEMENT__',
|
||||
logoText: '+',
|
||||
})
|
||||
return displays
|
||||
})
|
||||
|
||||
@@ -68,7 +73,7 @@ watch(curtShow, (value) => {
|
||||
<div ref="el" v-show="curtShow" class="atk-site-list-floater">
|
||||
<div class="atk-sites">
|
||||
<div
|
||||
v-for="(site) in displaySites"
|
||||
v-for="site in displaySites"
|
||||
class="atk-site-item"
|
||||
:class="{ 'atk-active': curtSite === site.name }"
|
||||
@click="switchSite(site.name)"
|
||||
@@ -120,7 +125,7 @@ watch(curtShow, (value) => {
|
||||
margin: 10px;
|
||||
border-radius: 3px;
|
||||
text-align: center;
|
||||
color: #FFF;
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
@@ -130,14 +135,16 @@ watch(curtShow, (value) => {
|
||||
margin-left: 7px;
|
||||
}
|
||||
|
||||
&.atk-active, &:hover {
|
||||
&.atk-active,
|
||||
&:hover {
|
||||
background: var(--at-color-bg-grey);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.v-enter-from, .v-leave-to {
|
||||
.v-enter-from,
|
||||
.v-leave-to {
|
||||
opacity: 0;
|
||||
transform: scale3d(1.05, 1.05, 1.05);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useNavStore } from '../stores/nav'
|
||||
import { useUserStore } from '../stores/user'
|
||||
import { storeToRefs } from 'pinia'
|
||||
|
||||
type PageItem = { label: string, link: string, hide?: boolean }
|
||||
type PageItem = { label: string; link: string; hide?: boolean }
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
@@ -12,8 +12,8 @@ const { t } = useI18n()
|
||||
|
||||
const { curtPage, curtTab, tabs, isSearchEnabled } = storeToRefs(nav)
|
||||
const { isAdmin } = storeToRefs(useUserStore())
|
||||
const indicator = ref<'pages'|'tabs'>('tabs')
|
||||
const tabListEl = ref<HTMLElement|null>(null)
|
||||
const indicator = ref<'pages' | 'tabs'>('tabs')
|
||||
const tabListEl = ref<HTMLElement | null>(null)
|
||||
|
||||
const pages = computed((): { [name: string]: PageItem } => {
|
||||
if (isAdmin.value) {
|
||||
@@ -42,22 +42,22 @@ const pages = computed((): { [name: string]: PageItem } => {
|
||||
},
|
||||
settings: {
|
||||
label: 'settings',
|
||||
link: '/settings'
|
||||
}
|
||||
link: '/settings',
|
||||
},
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
comments: {
|
||||
label: 'comment',
|
||||
link: '/comments',
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const isSearchShow = ref(false)
|
||||
const searchValue = ref('')
|
||||
const searchInputEl = ref<HTMLInputElement|null>(null)
|
||||
const searchInputEl = ref<HTMLInputElement | null>(null)
|
||||
|
||||
onMounted(() => {
|
||||
tabListEl.value!.addEventListener('wheel', (evt) => {
|
||||
@@ -67,7 +67,7 @@ onMounted(() => {
|
||||
})
|
||||
|
||||
function toggleIndicator() {
|
||||
indicator.value = (indicator.value !== 'tabs') ? 'tabs' : 'pages'
|
||||
indicator.value = indicator.value !== 'tabs' ? 'tabs' : 'pages'
|
||||
}
|
||||
|
||||
function switchPage(pageName: string) {
|
||||
@@ -119,8 +119,10 @@ function onSearchSubmit(evt: Event) {
|
||||
<template>
|
||||
<div class="tab">
|
||||
<div class="page" @click="toggleIndicator()">
|
||||
<div class="icon" :class="(indicator === 'tabs') ? 'menu' : 'arrow'"></div>
|
||||
<div class="text" v-if="pages[curtPage]?.label">{{ t(pages[curtPage].label) }}</div>
|
||||
<div class="icon" :class="indicator === 'tabs' ? 'menu' : 'arrow'"></div>
|
||||
<div class="text" v-if="pages[curtPage]?.label">
|
||||
{{ t(pages[curtPage].label) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ref="tabListEl" class="tab-list">
|
||||
@@ -131,7 +133,9 @@ function onSearchSubmit(evt: Event) {
|
||||
class="item"
|
||||
:class="{ active: curtTab === tabName }"
|
||||
@click="switchTab(tabName as string)"
|
||||
>{{ t(tabLabel) }}</div>
|
||||
>
|
||||
{{ t(tabLabel) }}
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="isSearchEnabled"
|
||||
@@ -148,7 +152,9 @@ function onSearchSubmit(evt: Event) {
|
||||
v-show="!page.hide"
|
||||
:class="{ active: pageName === curtPage }"
|
||||
@click="switchPage(pageName as string)"
|
||||
>{{ t(page.label) }}</div>
|
||||
>
|
||||
{{ t(page.label) }}
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
@@ -160,7 +166,13 @@ function onSearchSubmit(evt: Event) {
|
||||
<div class="item back-btn" @click="hideSearch()">
|
||||
<div class="icon arrow"></div>
|
||||
</div>
|
||||
<input ref="searchInputEl" type="text" :placeholder="t('searchHint')" v-model="searchValue" required>
|
||||
<input
|
||||
ref="searchInputEl"
|
||||
type="text"
|
||||
:placeholder="t('searchHint')"
|
||||
v-model="searchValue"
|
||||
required
|
||||
/>
|
||||
<button type="submit" class="item search-btn"></button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -65,21 +65,29 @@ function submit() {
|
||||
}
|
||||
|
||||
if (isCreateMode.value) {
|
||||
artalk!.ctx.getApi().user.userAdd(user.value!, user.value!.password)
|
||||
artalk!.ctx
|
||||
.getApi()
|
||||
.user.userAdd(user.value!, user.value!.password)
|
||||
.then((respUser) => {
|
||||
emit('update', respUser)
|
||||
}).catch((e) => {
|
||||
alert('用户创建错误:'+e.msg)
|
||||
}).finally(() => {
|
||||
})
|
||||
.catch((e) => {
|
||||
alert('用户创建错误:' + e.msg)
|
||||
})
|
||||
.finally(() => {
|
||||
isLoading.value = false
|
||||
})
|
||||
} else {
|
||||
artalk!.ctx.getApi().user.userEdit(user.value!, user.value!.password)
|
||||
artalk!.ctx
|
||||
.getApi()
|
||||
.user.userEdit(user.value!, user.value!.password)
|
||||
.then((respUser) => {
|
||||
emit('update', respUser)
|
||||
}).catch((e) => {
|
||||
alert('用户保存错误:'+e.msg)
|
||||
}).finally(() => {
|
||||
})
|
||||
.catch((e) => {
|
||||
alert('用户保存错误:' + e.msg)
|
||||
})
|
||||
.finally(() => {
|
||||
isLoading.value = false
|
||||
})
|
||||
}
|
||||
@@ -89,30 +97,73 @@ function submit() {
|
||||
<template>
|
||||
<div class="user-editor-layer">
|
||||
<div class="header">
|
||||
<div class="title">{{ isCreateMode ? t('userCreate') : t('userEdit') }}</div>
|
||||
<div class="title">
|
||||
{{ isCreateMode ? t('userCreate') : t('userEdit') }}
|
||||
</div>
|
||||
<div v-if="!isCreateMode" class="close-btn" @click="close()">
|
||||
<i class="atk-icon atk-icon-close"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!isCreateMode" class="user-log">
|
||||
<div><span>{{ t('comments') }}</span>{{(user as any).comment_count}}</div>
|
||||
<div><span>{{ t('last') }} IP</span>{{(user as any).last_ip || '-'}}</div>
|
||||
<div><span>{{ t('last') }} UA</span>
|
||||
<template v-if="showFullDetails || !(user as any).last_ua">{{(user as any).last_ua || '-'}}</template>
|
||||
<template v-else><span @click="showFullDetails = true" style="cursor: pointer;color: var(--at-color-main)">{{ t('Show') }}</span></template>
|
||||
<div>
|
||||
<span>{{ t('comments') }}</span>
|
||||
{{ (user as any).comment_count }}
|
||||
</div>
|
||||
<div>
|
||||
<span>{{ t('last') }} IP</span>
|
||||
{{ (user as any).last_ip || '-' }}
|
||||
</div>
|
||||
<div>
|
||||
<span>{{ t('last') }} UA</span>
|
||||
<template v-if="showFullDetails || !(user as any).last_ua">
|
||||
{{ (user as any).last_ua || '-' }}
|
||||
</template>
|
||||
<template v-else>
|
||||
<span
|
||||
@click="showFullDetails = true"
|
||||
style="cursor: pointer; color: var(--at-color-main)"
|
||||
>
|
||||
{{ t('Show') }}
|
||||
</span>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<form v-if="user" class="atk-form" @submit.prevent="submit()">
|
||||
<div class="atk-label required">{{ t('username') }}</div>
|
||||
<input v-model="user.name" type="text" placeholder="" autocomplete="off">
|
||||
<div class="atk-label required">{{ t('email') }}</div>
|
||||
<input v-model="user.email" type="text" placeholder="" autocomplete="off">
|
||||
<input
|
||||
v-model="user.name"
|
||||
type="text"
|
||||
placeholder=""
|
||||
autocomplete="off"
|
||||
/>
|
||||
<div class="atk-label required">{{ t('email') }}</div>
|
||||
<input
|
||||
v-model="user.email"
|
||||
type="text"
|
||||
placeholder=""
|
||||
autocomplete="off"
|
||||
/>
|
||||
<div class="atk-label">{{ t('link') }}</div>
|
||||
<input v-model="user.link" type="text" placeholder="" autocomplete="off">
|
||||
<input
|
||||
v-model="user.link"
|
||||
type="text"
|
||||
placeholder=""
|
||||
autocomplete="off"
|
||||
/>
|
||||
<div class="atk-label">{{ t('badgeText') }}</div>
|
||||
<input v-model="user.badge_name" type="text" placeholder="" autocomplete="off">
|
||||
<input
|
||||
v-model="user.badge_name"
|
||||
type="text"
|
||||
placeholder=""
|
||||
autocomplete="off"
|
||||
/>
|
||||
<div class="atk-label">{{ t('badgeColor') }} (Color Hex)</div>
|
||||
<input v-model="user.badge_color" type="text" placeholder="" autocomplete="off">
|
||||
<input
|
||||
v-model="user.badge_color"
|
||||
type="text"
|
||||
placeholder=""
|
||||
autocomplete="off"
|
||||
/>
|
||||
<div class="atk-label required">{{ t('role') }}</div>
|
||||
<select v-model="user.is_admin">
|
||||
<option :value="false">{{ t('normal') }}</option>
|
||||
@@ -120,9 +171,19 @@ function submit() {
|
||||
</select>
|
||||
<template v-if="user.is_admin">
|
||||
<div class="atk-label required">{{ t('password') }}</div>
|
||||
<input v-model="user.password" type="text" :placeholder="isCreateMode ? '' : `(${t('passwordEmptyHint')})`" autocomplete="off">
|
||||
<input
|
||||
v-model="user.password"
|
||||
type="text"
|
||||
:placeholder="isCreateMode ? '' : `(${t('passwordEmptyHint')})`"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<div class="atk-label">{{ t('siteAttached') }}</div>
|
||||
<input v-model="user.site_names_raw" type="text" :placeholder="`(${t('siteEmptyHint')})`" autocomplete="off">
|
||||
<input
|
||||
v-model="user.site_names_raw"
|
||||
type="text"
|
||||
:placeholder="`(${t('siteEmptyHint')})`"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</template>
|
||||
<div class="atk-label required">{{ t('emailNotify') }}</div>
|
||||
<select v-model="user.receive_email">
|
||||
|
||||
@@ -2,7 +2,7 @@ import Artalk from 'artalk'
|
||||
import type { ArtalkType } from 'artalk'
|
||||
import { useUserStore } from './stores/user'
|
||||
|
||||
export let artalk: Artalk|null = null
|
||||
export let artalk: Artalk | null = null
|
||||
|
||||
// 启动参数
|
||||
export const bootParams = getBootParams()
|
||||
@@ -10,13 +10,13 @@ export function getBootParams() {
|
||||
const p = new URLSearchParams(document.location.search)
|
||||
|
||||
return {
|
||||
pageKey: p.get('pageKey') || '',
|
||||
site: p.get('site') || '',
|
||||
user: <ArtalkType.LocalUser>JSON.parse(p.get('user') || '{}'),
|
||||
view: p.get('view') || '',
|
||||
pageKey: p.get('pageKey') || '',
|
||||
site: p.get('site') || '',
|
||||
user: <ArtalkType.LocalUser>JSON.parse(p.get('user') || '{}'),
|
||||
view: p.get('view') || '',
|
||||
viewParams: <any>null,
|
||||
darkMode: p.get('darkMode') === '1',
|
||||
locale: p.get('locale') || '',
|
||||
darkMode: p.get('darkMode') === '1',
|
||||
locale: p.get('locale') || '',
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ export function createArtalkInstance() {
|
||||
|
||||
return Artalk.init({
|
||||
el: artalkEl,
|
||||
server: (import.meta.env.DEV) ? 'http://localhost:23366' : '../',
|
||||
server: import.meta.env.DEV ? 'http://localhost:23366' : '../',
|
||||
pageKey: bootParams.pageKey,
|
||||
site: bootParams.site,
|
||||
darkMode: bootParams.darkMode,
|
||||
@@ -41,13 +41,14 @@ export function createArtalkInstance() {
|
||||
autoLoad: false,
|
||||
}
|
||||
conf.listUnreadHighlight = true
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function importUserDataFromArtalkInstance() {
|
||||
if (!artalk) throw Error("the artalk instance is not exist")
|
||||
if (!artalk.ctx.get('user').getData().email) throw Error("the user data in artalk instance is invalid")
|
||||
if (!artalk) throw Error('the artalk instance is not exist')
|
||||
if (!artalk.ctx.get('user').getData().email)
|
||||
throw Error('the user data in artalk instance is invalid')
|
||||
|
||||
const userData = artalk.ctx.get('user').getData()
|
||||
useUserStore().$patch((state) => {
|
||||
|
||||
@@ -22,7 +22,8 @@ const en = {
|
||||
export: 'Export',
|
||||
settingSaved: 'Setting saved',
|
||||
settingSaveFailed: 'Setting save failed',
|
||||
settingNotice: 'Note: Some config options may require a manual reboot to take effect.',
|
||||
settingNotice:
|
||||
'Note: Some config options may require a manual reboot to take effect.',
|
||||
apply: 'Apply',
|
||||
updateComplete: 'Update complete',
|
||||
updateReady: 'Ready to update...',
|
||||
@@ -76,7 +77,7 @@ const en = {
|
||||
uploadReadyToImport: 'File uploaded and is ready for import',
|
||||
loginFailure: 'Login failure',
|
||||
login: 'Login',
|
||||
loginSelectHint: 'Please select the account you wish to log into:'
|
||||
loginSelectHint: 'Please select the account you wish to log into:',
|
||||
}
|
||||
|
||||
const zhCN: typeof en = {
|
||||
@@ -242,5 +243,7 @@ const zhTW: typeof en = {
|
||||
}
|
||||
|
||||
export default {
|
||||
en, 'zh-CN': zhCN, 'zh-TW': zhTW
|
||||
en,
|
||||
'zh-CN': zhCN,
|
||||
'zh-TW': zhTW,
|
||||
}
|
||||
@@ -1,8 +1,13 @@
|
||||
import type YAML from 'yaml'
|
||||
|
||||
type YAMLPair = {
|
||||
key?: { value: string, commentBefore: string, comment: string },
|
||||
value?: { commentBefore: string, comment: string, value?: any, items?: YAMLPair[] }
|
||||
key?: { value: string; commentBefore: string; comment: string }
|
||||
value?: {
|
||||
commentBefore: string
|
||||
comment: string
|
||||
value?: any
|
||||
items?: YAMLPair[]
|
||||
}
|
||||
}
|
||||
|
||||
export function createSettings(yamlObj: any) {
|
||||
@@ -18,7 +23,7 @@ export function createSettings(yamlObj: any) {
|
||||
const key = item?.key?.value
|
||||
if (!key) return
|
||||
|
||||
const itemPath = (!path) ? [key] : [...path, key]
|
||||
const itemPath = !path ? [key] : [...path, key]
|
||||
|
||||
let comment = ''
|
||||
if (index === 0 && parent) comment = parent?.value?.commentBefore || ''
|
||||
@@ -35,7 +40,7 @@ export function createSettings(yamlObj: any) {
|
||||
})
|
||||
}
|
||||
|
||||
function extractItemDescFromComment(nodePath: string|(string|number)[]) {
|
||||
function extractItemDescFromComment(nodePath: string | (string | number)[]) {
|
||||
if (Array.isArray(nodePath)) nodePath = nodePath.join('.')
|
||||
const nodeName = nodePath.split('.').slice(-1)[0]
|
||||
let comment = (comments[nodePath] || '').trim()
|
||||
@@ -45,42 +50,47 @@ export function createSettings(yamlObj: any) {
|
||||
|
||||
let title = ''
|
||||
let subTitle = ''
|
||||
let opts: string[]|null = null
|
||||
let opts: string[] | null = null
|
||||
|
||||
const stReg = /\(.*?\)/gm
|
||||
title = comment.replace(stReg, '').trim()
|
||||
const stFind = stReg.exec(comment)
|
||||
subTitle = stFind ? stFind[0].substring(1, stFind[0].length-1) : ''
|
||||
subTitle = stFind ? stFind[0].substring(1, stFind[0].length - 1) : ''
|
||||
if (!title) {
|
||||
const commonDict: any = { 'enabled': '启用' }
|
||||
const commonDict: any = { enabled: '启用' }
|
||||
title = commonDict[nodeName] || snakeToCamel(nodeName)
|
||||
}
|
||||
|
||||
const optReg = /\[.*?\]/gm
|
||||
const optFind = optReg.exec(title)
|
||||
if (optFind) {
|
||||
try { opts = JSON.parse(optFind[0]) } catch (err) { console.error(err) }
|
||||
try {
|
||||
opts = JSON.parse(optFind[0])
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
title = title.replace(optReg, '').trim()
|
||||
}
|
||||
|
||||
return {
|
||||
title, subTitle, opts
|
||||
title,
|
||||
subTitle,
|
||||
opts,
|
||||
}
|
||||
}
|
||||
|
||||
function snakeToCamel(str: string) {
|
||||
return str.toLowerCase()
|
||||
.replace(/([_][a-z]|^[a-z])/g, (group) =>
|
||||
group.slice(-1).toUpperCase()
|
||||
)
|
||||
return str
|
||||
.toLowerCase()
|
||||
.replace(/([_][a-z]|^[a-z])/g, (group) => group.slice(-1).toUpperCase())
|
||||
}
|
||||
|
||||
return { comments, customs, defaultValues, extractItemDescFromComment }
|
||||
}
|
||||
|
||||
let instance: ReturnType<(typeof createSettings)>
|
||||
let instance: ReturnType<typeof createSettings>
|
||||
|
||||
export default {
|
||||
init: (yamlObj: any) => instance = createSettings(yamlObj),
|
||||
get: () => instance
|
||||
init: (yamlObj: any) => (instance = createSettings(yamlObj)),
|
||||
get: () => instance,
|
||||
}
|
||||
@@ -6,7 +6,11 @@ import messages from './i18n/messages'
|
||||
import 'artalk/dist/Artalk.css'
|
||||
import './style.scss'
|
||||
import App from './App.vue'
|
||||
import global, { createArtalkInstance, bootParams, getBootParams } from './global'
|
||||
import global, {
|
||||
createArtalkInstance,
|
||||
bootParams,
|
||||
getBootParams,
|
||||
} from './global'
|
||||
|
||||
// 初始化 Artalk
|
||||
global.setArtalk(createArtalkInstance())
|
||||
@@ -25,7 +29,7 @@ const i18n = createI18n({
|
||||
legacy: false, // use i18n in Composition API
|
||||
locale: getBootParams().locale || 'en',
|
||||
fallbackLocale: 'en',
|
||||
messages
|
||||
messages,
|
||||
})
|
||||
|
||||
app.use(i18n)
|
||||
|
||||
@@ -18,23 +18,29 @@ const search = ref('')
|
||||
onMounted(() => {
|
||||
// 初始化导航条
|
||||
if (user.isAdmin) {
|
||||
nav.updateTabs({
|
||||
admin_all: 'all',
|
||||
admin_pending: 'pending',
|
||||
all: 'personal',
|
||||
}, 'admin_all')
|
||||
nav.updateTabs(
|
||||
{
|
||||
admin_all: 'all',
|
||||
admin_pending: 'pending',
|
||||
all: 'personal',
|
||||
},
|
||||
'admin_all',
|
||||
)
|
||||
} else {
|
||||
nav.updateTabs({
|
||||
all: 'all',
|
||||
mentions: 'mentions',
|
||||
mine: 'mine',
|
||||
pending: 'pending',
|
||||
}, 'all')
|
||||
nav.updateTabs(
|
||||
{
|
||||
all: 'all',
|
||||
mentions: 'mentions',
|
||||
mine: 'mine',
|
||||
pending: 'pending',
|
||||
},
|
||||
'all',
|
||||
)
|
||||
}
|
||||
|
||||
watch(curtTab, (curtTab) => {
|
||||
artalk!.ctx.fetch({
|
||||
offset: 0
|
||||
offset: 0,
|
||||
})
|
||||
})
|
||||
|
||||
@@ -71,14 +77,17 @@ onMounted(() => {
|
||||
listEl.value?.append($el)
|
||||
|
||||
// 搜索功能
|
||||
nav.enableSearch((value: string) => {
|
||||
search.value = value
|
||||
artalk!.reload()
|
||||
}, () => {
|
||||
if (search.value === '') return
|
||||
search.value = ''
|
||||
artalk!.reload()
|
||||
})
|
||||
nav.enableSearch(
|
||||
(value: string) => {
|
||||
search.value = value
|
||||
artalk!.reload()
|
||||
},
|
||||
() => {
|
||||
if (search.value === '') return
|
||||
search.value = ''
|
||||
artalk!.reload()
|
||||
},
|
||||
)
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -18,11 +18,11 @@ onMounted(() => {
|
||||
if (splitted[1]) bootParams.viewParams = JSON.parse(splitted[1])
|
||||
}
|
||||
|
||||
const LinkMap: {[key:string]:string} = {
|
||||
const LinkMap: { [key: string]: string } = {
|
||||
comments: '/comments',
|
||||
pages: '/pages',
|
||||
sites: '/sites',
|
||||
settings: '/settings'
|
||||
settings: '/settings',
|
||||
}
|
||||
router.replace(LinkMap[bootParams.view] || '/comments')
|
||||
})
|
||||
|
||||
@@ -13,19 +13,23 @@ const router = useRouter()
|
||||
const { t } = useI18n()
|
||||
|
||||
let userForm = ref({
|
||||
email: '',
|
||||
password: ''
|
||||
email: '',
|
||||
password: '',
|
||||
})
|
||||
let version = ref('')
|
||||
let buildHash = ref('')
|
||||
let loginErr = ref('')
|
||||
let userSelector = ref<string[]|null>(null)
|
||||
let userSelector = ref<string[] | null>(null)
|
||||
|
||||
onMounted(() => {
|
||||
global.getArtalk()!.ctx.getApi().system.version().then((v) => {
|
||||
version.value = v.version
|
||||
buildHash.value = v.commit_hash
|
||||
})
|
||||
global
|
||||
.getArtalk()!
|
||||
.ctx.getApi()
|
||||
.system.version()
|
||||
.then((v) => {
|
||||
version.value = v.version
|
||||
buildHash.value = v.commit_hash
|
||||
})
|
||||
})
|
||||
|
||||
function onFocus() {
|
||||
@@ -34,49 +38,78 @@ function onFocus() {
|
||||
|
||||
function login(username?: string) {
|
||||
loginErr.value = ''
|
||||
global.getArtalk()!.ctx.getApi().user.login(
|
||||
username || '', userForm.value.email, userForm.value.password
|
||||
).then((resp) => {
|
||||
const user = resp.user
|
||||
global.getArtalk()!.ctx.get('user').update({
|
||||
nick: user.name,
|
||||
email: user.email,
|
||||
link: user.link,
|
||||
isAdmin: user.is_admin,
|
||||
token: resp.token,
|
||||
global
|
||||
.getArtalk()!
|
||||
.ctx.getApi()
|
||||
.user.login(username || '', userForm.value.email, userForm.value.password)
|
||||
.then((resp) => {
|
||||
const user = resp.user
|
||||
global.getArtalk()!.ctx.get('user').update({
|
||||
nick: user.name,
|
||||
email: user.email,
|
||||
link: user.link,
|
||||
isAdmin: user.is_admin,
|
||||
token: resp.token,
|
||||
})
|
||||
global.importUserDataFromArtalkInstance()
|
||||
router.replace('/')
|
||||
})
|
||||
.catch((e) => {
|
||||
if (e.data?.need_name_select) {
|
||||
userSelector.value = e.data?.need_name_select
|
||||
} else {
|
||||
loginErr.value = e.msg || t('loginFailure')
|
||||
}
|
||||
})
|
||||
global.importUserDataFromArtalkInstance()
|
||||
router.replace('/')
|
||||
}).catch((e) => {
|
||||
if (e.data?.need_name_select) {
|
||||
userSelector.value = e.data?.need_name_select
|
||||
} else {
|
||||
loginErr.value = e.msg || t('loginFailure')
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="login-dialog">
|
||||
<a href="https://artalk.js.org/" target="_blank">
|
||||
<img class="logo" src="../assets/favicon.png" alt="logo" draggable="false" />
|
||||
<img
|
||||
class="logo"
|
||||
src="../assets/favicon.png"
|
||||
alt="logo"
|
||||
draggable="false"
|
||||
/>
|
||||
</a>
|
||||
<form class="login-form" @submit.prevent="login()">
|
||||
<input type="text" :placeholder="t('email')" v-model="userForm.email" @focus="onFocus">
|
||||
<input type="password" :placeholder="t('password')" v-model="userForm.password" @focus="onFocus">
|
||||
<div v-if="!!loginErr" class="err-msg atk-fade-in">{{loginErr}}</div>
|
||||
<input
|
||||
type="text"
|
||||
:placeholder="t('email')"
|
||||
v-model="userForm.email"
|
||||
@focus="onFocus"
|
||||
/>
|
||||
<input
|
||||
type="password"
|
||||
:placeholder="t('password')"
|
||||
v-model="userForm.password"
|
||||
@focus="onFocus"
|
||||
/>
|
||||
<div v-if="!!loginErr" class="err-msg atk-fade-in">{{ loginErr }}</div>
|
||||
<button type="submit">{{ t('login') }}</button>
|
||||
</form>
|
||||
<div class="copyright">
|
||||
Powered by <a href="https://artalk.js.org" target="_blank">Artalk</a> (v{{version}} / {{buildHash}})
|
||||
Powered by
|
||||
<a href="https://artalk.js.org" target="_blank">Artalk</a>
|
||||
(v{{ version }} / {{ buildHash }})
|
||||
</div>
|
||||
|
||||
<div v-if="userSelector" class="layer">
|
||||
<div class="user-selector atk-fade-in">
|
||||
<div class="text">{{ t('loginSelectHint') }}</div>
|
||||
<div class="user-list">
|
||||
<div v-for="u in userSelector" class="item" @click="userSelector = null; login(u)">{{u}}</div>
|
||||
<div
|
||||
v-for="u in userSelector"
|
||||
class="item"
|
||||
@click="
|
||||
userSelector = null
|
||||
login(u)
|
||||
"
|
||||
>
|
||||
{{ u }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -148,7 +181,7 @@ function login(username?: string) {
|
||||
display: block;
|
||||
|
||||
&:hover {
|
||||
opacity: .95;
|
||||
opacity: 0.95;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -203,7 +236,7 @@ function login(username?: string) {
|
||||
& > .item {
|
||||
cursor: pointer;
|
||||
padding: 10px 20px;
|
||||
transition: border .3s ease;
|
||||
transition: border 0.3s ease;
|
||||
border-left: 2px solid transparent;
|
||||
|
||||
&:hover {
|
||||
|
||||
@@ -9,7 +9,7 @@ import Pagination from '../components/Pagination.vue'
|
||||
const nav = useNavStore()
|
||||
const { site: curtSite } = storeToRefs(useUserStore())
|
||||
const pages = ref<ArtalkType.PageData[]>([])
|
||||
const curtEditPageID = ref<number|null>(null)
|
||||
const curtEditPageID = ref<number | null>(null)
|
||||
const { t } = useI18n()
|
||||
|
||||
const pageSize = ref(20)
|
||||
@@ -22,9 +22,7 @@ const refreshBtn = ref({
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
nav.updateTabs({
|
||||
|
||||
}, '')
|
||||
nav.updateTabs({}, '')
|
||||
|
||||
reqPages(0)
|
||||
|
||||
@@ -34,7 +32,7 @@ onMounted(() => {
|
||||
})
|
||||
|
||||
// Refresh task status recovery
|
||||
getRefreshTaskStatus().then(d => {
|
||||
getRefreshTaskStatus().then((d) => {
|
||||
if (d.is_progress === true) {
|
||||
refreshBtn.value.isRun = true
|
||||
refreshBtn.value.statusText = d.msg
|
||||
@@ -50,7 +48,7 @@ onUnmounted(() => {
|
||||
})
|
||||
|
||||
function scrollHandler() {
|
||||
showActBarBorder.value = (nav.scrollableArea!.scrollTop > 10)
|
||||
showActBarBorder.value = nav.scrollableArea!.scrollTop > 10
|
||||
}
|
||||
|
||||
function editPage(page: ArtalkType.PageData) {
|
||||
@@ -59,12 +57,15 @@ function editPage(page: ArtalkType.PageData) {
|
||||
|
||||
function reqPages(offset: number) {
|
||||
nav.setPageLoading(true)
|
||||
artalk?.ctx.getApi().page.pageGet(curtSite.value, offset, pageSize.value)
|
||||
.then(data => {
|
||||
artalk?.ctx
|
||||
.getApi()
|
||||
.page.pageGet(curtSite.value, offset, pageSize.value)
|
||||
.then((data) => {
|
||||
pageTotal.value = data.total
|
||||
pages.value = data.pages
|
||||
nav.scrollPageToTop()
|
||||
}).finally(() => {
|
||||
})
|
||||
.finally(() => {
|
||||
nav.setPageLoading(false)
|
||||
})
|
||||
}
|
||||
@@ -74,22 +75,27 @@ function onChangePage(offset: number) {
|
||||
}
|
||||
|
||||
function onPageItemUpdate(page: ArtalkType.PageData) {
|
||||
const index = pages.value.findIndex(p => p.id === page.id)
|
||||
const index = pages.value.findIndex((p) => p.id === page.id)
|
||||
if (index != -1) {
|
||||
const orgPage = pages.value[index]
|
||||
Object.keys(page).forEach(key => {
|
||||
Object.keys(page).forEach((key) => {
|
||||
;(orgPage as any)[key] = (page as any)[key]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function onPageItemRemove(id: number) {
|
||||
const index = pages.value.findIndex(p => p.id === id)
|
||||
const index = pages.value.findIndex((p) => p.id === id)
|
||||
pages.value.splice(index, 1)
|
||||
}
|
||||
|
||||
async function getRefreshTaskStatus() {
|
||||
return await artalk!.ctx.getApi().page.pageFetch(undefined, undefined, true) as { is_progress: boolean, msg: string }
|
||||
return (await artalk!.ctx
|
||||
.getApi()
|
||||
.page.pageFetch(undefined, undefined, true)) as {
|
||||
is_progress: boolean
|
||||
msg: string
|
||||
}
|
||||
}
|
||||
|
||||
function startRefreshTaskWatchdog() {
|
||||
@@ -132,11 +138,19 @@ async function refreshAllPages() {
|
||||
}
|
||||
|
||||
function cacheFlush() {
|
||||
artalk!.ctx.getApi().admin.cacheFlushAll().then((d: any) => alert(d.msg)).catch(() => alert(t('opFailed')))
|
||||
artalk!.ctx
|
||||
.getApi()
|
||||
.admin.cacheFlushAll()
|
||||
.then((d: any) => alert(d.msg))
|
||||
.catch(() => alert(t('opFailed')))
|
||||
}
|
||||
|
||||
function cacheWarm() {
|
||||
artalk!.ctx.getApi().admin.cacheWarmUp().then((d: any) => alert(d.msg)).catch(() => alert(t('opFailed')))
|
||||
artalk!.ctx
|
||||
.getApi()
|
||||
.admin.cacheWarmUp()
|
||||
.then((d: any) => alert(d.msg))
|
||||
.catch(() => alert(t('opFailed')))
|
||||
}
|
||||
|
||||
function openPage(url: string) {
|
||||
@@ -146,18 +160,29 @@ function openPage(url: string) {
|
||||
|
||||
<template>
|
||||
<div class="atk-page-list-wrap">
|
||||
<div class="atk-header-action-bar" :class="{ 'bordered': showActBarBorder }">
|
||||
<div class="atk-header-action-bar" :class="{ bordered: showActBarBorder }">
|
||||
<span class="atk-update-all-title-btn" @click="refreshAllPages()">
|
||||
<i class="atk-icon atk-icon-sync" :class="{'atk-rotate': refreshBtn.isRun}"></i>
|
||||
<span class="atk-text">{{ refreshBtn.isRun ? refreshBtn.statusText : t('updateTitle') }}</span>
|
||||
<i
|
||||
class="atk-icon atk-icon-sync"
|
||||
:class="{ 'atk-rotate': refreshBtn.isRun }"
|
||||
></i>
|
||||
<span class="atk-text">
|
||||
{{ refreshBtn.isRun ? refreshBtn.statusText : t('updateTitle') }}
|
||||
</span>
|
||||
</span>
|
||||
<span class="atk-cache-flush-all-btn" @click="cacheFlush()">
|
||||
<span class="atk-text">{{ t('cacheClear') }}</span>
|
||||
</span>
|
||||
<span class="atk-cache-warm-up-btn" @click="cacheWarm()">
|
||||
<span class="atk-text">{{ t('cacheWarm') }}</span>
|
||||
</span>
|
||||
<span class="atk-cache-flush-all-btn" @click="cacheFlush()"><span class="atk-text">{{ t('cacheClear') }}</span></span>
|
||||
<span class="atk-cache-warm-up-btn" @click="cacheWarm()"><span class="atk-text">{{ t('cacheWarm') }}</span></span>
|
||||
</div>
|
||||
<div class="atk-page-list">
|
||||
<div v-for="(page) in pages" class="atk-page-item">
|
||||
<div v-for="page in pages" class="atk-page-item">
|
||||
<div class="atk-page-main">
|
||||
<div class="atk-title" @click="openPage(page.url)">{{ page.title }}</div>
|
||||
<div class="atk-title" @click="openPage(page.url)">
|
||||
{{ page.title }}
|
||||
</div>
|
||||
<div class="atk-sub" @click="openPage(page.url)">{{ page.url }}</div>
|
||||
</div>
|
||||
<div class="atk-page-actions">
|
||||
@@ -196,7 +221,7 @@ function openPage(url: string) {
|
||||
background: var(--at-color-bg);
|
||||
z-index: 10;
|
||||
border-bottom: 1px solid transparent;
|
||||
transition: .3s ease-out padding;
|
||||
transition: 0.3s ease-out padding;
|
||||
|
||||
&.bordered {
|
||||
padding-bottom: 10px;
|
||||
|
||||
@@ -15,8 +15,8 @@ let settingsTpl = shallowReactive({ tpl: null as any })
|
||||
|
||||
onMounted(() => {
|
||||
nav.updateTabs({
|
||||
'sites': 'site',
|
||||
'transfer': 'transfer',
|
||||
sites: 'site',
|
||||
transfer: 'transfer',
|
||||
})
|
||||
|
||||
watch(curtTab, (tab) => {
|
||||
@@ -24,12 +24,15 @@ onMounted(() => {
|
||||
else if (tab === 'transfer') router.replace('/transfer')
|
||||
})
|
||||
|
||||
artalk!.ctx.getApi().system.getSettings().then((data) => {
|
||||
const yamlObj = YAML.parseDocument(data.template)
|
||||
settingsTpl.tpl = yamlObj
|
||||
settings.init(yamlObj)
|
||||
settings.get().customs.value = YAML.parseDocument(data.custom)
|
||||
})
|
||||
artalk!.ctx
|
||||
.getApi()
|
||||
.system.getSettings()
|
||||
.then((data) => {
|
||||
const yamlObj = YAML.parseDocument(data.template)
|
||||
settingsTpl.tpl = yamlObj
|
||||
settings.init(yamlObj)
|
||||
settings.get().customs.value = YAML.parseDocument(data.custom)
|
||||
})
|
||||
})
|
||||
|
||||
function save() {
|
||||
@@ -37,7 +40,7 @@ function save() {
|
||||
try {
|
||||
yamlStr = settings.get().customs.value?.toString() || ''
|
||||
} catch (err) {
|
||||
alert('YAML export error: '+err)
|
||||
alert('YAML export error: ' + err)
|
||||
console.error(err)
|
||||
return
|
||||
}
|
||||
@@ -50,14 +53,19 @@ function save() {
|
||||
|
||||
if (isLoading.value) return
|
||||
isLoading.value = true
|
||||
artalk!.ctx.getApi().system.saveSettings(yamlStr).then(() => {
|
||||
alert(t('settingSaved'))
|
||||
}).catch((err) => {
|
||||
console.error(err)
|
||||
alert(t('settingSaveFailed')+': '+err)
|
||||
}).finally(() => {
|
||||
isLoading.value = false
|
||||
})
|
||||
artalk!.ctx
|
||||
.getApi()
|
||||
.system.saveSettings(yamlStr)
|
||||
.then(() => {
|
||||
alert(t('settingSaved'))
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err)
|
||||
alert(t('settingSaveFailed') + ': ' + err)
|
||||
})
|
||||
.finally(() => {
|
||||
isLoading.value = false
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -65,14 +73,14 @@ function save() {
|
||||
<div v-if="!!settingsTpl.tpl" class="settings">
|
||||
<div class="act-bar">
|
||||
<div class="status-text"></div>
|
||||
<button class="save-btn" @click="save()"><i class="atk-icon atk-icon-yes" /> {{ t('apply') }}</button>
|
||||
<button class="save-btn" @click="save()">
|
||||
<i class="atk-icon atk-icon-yes" />
|
||||
{{ t('apply') }}
|
||||
</button>
|
||||
<LoadingLayer v-if="isLoading" />
|
||||
</div>
|
||||
<div class="pfs">
|
||||
<PreferenceGrp
|
||||
:tpl-data="settingsTpl.tpl.toJS()"
|
||||
:path="[]"
|
||||
/>
|
||||
<PreferenceGrp :tpl-data="settingsTpl.tpl.toJS()" :path="[]" />
|
||||
<div class="notice">{{ t('settingNotice') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -124,7 +132,7 @@ function save() {
|
||||
border: 0;
|
||||
|
||||
&:active {
|
||||
opacity: .9;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
i {
|
||||
@@ -141,7 +149,8 @@ function save() {
|
||||
padding: 10px 30px;
|
||||
}
|
||||
|
||||
:deep(input[type="text"]), :deep(select) {
|
||||
:deep(input[type='text']),
|
||||
:deep(select) {
|
||||
font-size: 17px;
|
||||
width: 100%;
|
||||
height: 35px;
|
||||
|
||||
@@ -5,22 +5,24 @@ import type { ArtalkType } from 'artalk'
|
||||
|
||||
const nav = useNavStore()
|
||||
const sites = ref<ArtalkType.SiteData[]>([])
|
||||
const curtEditSite = ref<ArtalkType.SiteData|null>(null)
|
||||
const curtEditSite = ref<ArtalkType.SiteData | null>(null)
|
||||
const showSiteCreate = ref(false)
|
||||
const siteCreateInitVal = ref()
|
||||
const { t } = useI18n()
|
||||
|
||||
onMounted(() => {
|
||||
nav.updateTabs({
|
||||
|
||||
}, 'sites')
|
||||
nav.updateTabs({}, 'sites')
|
||||
|
||||
nav.setPageLoading(true)
|
||||
artalk?.ctx.getApi().site.siteGet().then(gotSites => {
|
||||
sites.value = gotSites
|
||||
}).finally(() => {
|
||||
nav.setPageLoading(false)
|
||||
})
|
||||
artalk?.ctx
|
||||
.getApi()
|
||||
.site.siteGet()
|
||||
.then((gotSites) => {
|
||||
sites.value = gotSites
|
||||
})
|
||||
.finally(() => {
|
||||
nav.setPageLoading(false)
|
||||
})
|
||||
|
||||
// 通过启动参数打开站点创建
|
||||
const vp = bootParams.viewParams
|
||||
@@ -46,7 +48,8 @@ const sitesGrouped = computed(() => {
|
||||
let j = -1
|
||||
for (let i = 0; i < sites.value.length; i++) {
|
||||
const item = sites.value[i]
|
||||
if (i % 4 === 0) { // 每 4 个一组
|
||||
if (i % 4 === 0) {
|
||||
// 每 4 个一组
|
||||
grp.push([])
|
||||
j++
|
||||
}
|
||||
@@ -67,10 +70,10 @@ function onNewSiteCreated(siteNew: ArtalkType.SiteData) {
|
||||
}
|
||||
|
||||
function onSiteItemUpdate(site: ArtalkType.SiteData) {
|
||||
const index = sites.value.findIndex(s => s.id === site.id)
|
||||
const index = sites.value.findIndex((s) => s.id === site.id)
|
||||
if (index != -1) {
|
||||
const orgSite = sites.value[index]
|
||||
Object.keys(site).forEach(key => {
|
||||
Object.keys(site).forEach((key) => {
|
||||
;(orgSite as any)[key] = (site as any)[key]
|
||||
})
|
||||
}
|
||||
@@ -78,7 +81,7 @@ function onSiteItemUpdate(site: ArtalkType.SiteData) {
|
||||
}
|
||||
|
||||
function onSiteItemRemove(id: number) {
|
||||
const index = sites.value.findIndex(p => p.id === id)
|
||||
const index = sites.value.findIndex((p) => p.id === id)
|
||||
sites.value.splice(index, 1)
|
||||
nav.refreshSites()
|
||||
}
|
||||
@@ -89,7 +92,9 @@ function onSiteItemRemove(id: number) {
|
||||
<div class="atk-header">
|
||||
<div class="atk-title">{{ t('siteCount', { count: sites.length }) }}</div>
|
||||
<div class="atk-actions">
|
||||
<div class="atk-item atk-site-add-btn" @click="create()"><i class="atk-icon atk-icon-plus"></i></div>
|
||||
<div class="atk-item atk-site-add-btn" @click="create()">
|
||||
<i class="atk-icon atk-icon-plus"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<SiteCreate
|
||||
@@ -99,7 +104,7 @@ function onSiteItemRemove(id: number) {
|
||||
@done="onNewSiteCreated"
|
||||
/>
|
||||
<div class="atk-site-rows-wrap">
|
||||
<template v-for="(sites) in sitesGrouped">
|
||||
<template v-for="sites in sitesGrouped">
|
||||
<template v-if="curtEditSite !== null">
|
||||
<SiteEditor
|
||||
v-if="!!sites.includes(curtEditSite)"
|
||||
@@ -111,7 +116,7 @@ function onSiteItemRemove(id: number) {
|
||||
</template>
|
||||
<div class="atk-site-row">
|
||||
<div
|
||||
v-for="(site) in sites"
|
||||
v-for="site in sites"
|
||||
class="atk-site-item"
|
||||
:class="{ 'atk-active': curtEditSite === site }"
|
||||
@click="edit(site)"
|
||||
@@ -218,7 +223,8 @@ function onSiteItemRemove(id: number) {
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.atk-site-edit), :deep(.atk-site-add) {
|
||||
:deep(.atk-site-edit),
|
||||
:deep(.atk-site-add) {
|
||||
position: relative;
|
||||
min-height: 120px;
|
||||
width: 100%;
|
||||
@@ -247,7 +253,7 @@ function onSiteItemRemove(id: number) {
|
||||
width: 100%;
|
||||
height: 6px;
|
||||
background: var(--at-color-main);
|
||||
opacity: .4;
|
||||
opacity: 0.4;
|
||||
left: 0;
|
||||
bottom: 6px;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { useNavStore } from '../stores/nav'
|
||||
import { useUserStore } from '../stores/user'
|
||||
import { artalk } from '../global'
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { storeToRefs } from 'pinia'
|
||||
|
||||
const nav = useNavStore()
|
||||
const user = useUserStore()
|
||||
@@ -12,7 +12,7 @@ const { t } = useI18n()
|
||||
const importParams = ref({
|
||||
siteName: '',
|
||||
siteURL: '',
|
||||
payload: ''
|
||||
payload: '',
|
||||
})
|
||||
|
||||
const isLoading = ref(false)
|
||||
@@ -23,15 +23,18 @@ const importTaskApiURL = ref('')
|
||||
const uploadedFilename = ref('')
|
||||
|
||||
const importTaskStarted = ref(false)
|
||||
const importTaskParams = ref<{[k:string]:string}>({})
|
||||
const importTaskParams = ref<{ [k: string]: string }>({})
|
||||
|
||||
const exportTaskStarted = ref(false)
|
||||
|
||||
onMounted(() => {
|
||||
nav.updateTabs({
|
||||
'import': 'import',
|
||||
'export': 'export',
|
||||
}, 'import')
|
||||
nav.updateTabs(
|
||||
{
|
||||
import: 'import',
|
||||
export: 'export',
|
||||
},
|
||||
'import',
|
||||
)
|
||||
watch(curtTab, (tab) => {
|
||||
if (tab === 'export') {
|
||||
startExportTask()
|
||||
@@ -113,7 +116,10 @@ async function startExportTask() {
|
||||
|
||||
function downloadFile(filename: string, text: string) {
|
||||
const el = document.createElement('a')
|
||||
el.setAttribute('href', `data:text/json;charset=utf-8,${encodeURIComponent(text)}`);
|
||||
el.setAttribute(
|
||||
'href',
|
||||
`data:text/json;charset=utf-8,${encodeURIComponent(text)}`,
|
||||
)
|
||||
el.setAttribute('download', filename)
|
||||
el.style.display = 'none'
|
||||
document.body.appendChild(el)
|
||||
@@ -131,7 +137,10 @@ function getYmdHisFilename() {
|
||||
const minutes = date.getMinutes()
|
||||
const seconds = date.getSeconds()
|
||||
|
||||
return `${year}${month}${day}-${hours}${padWithZeros(minutes, 2)}${padWithZeros(seconds, 2)}`
|
||||
return `${year}${month}${day}-${hours}${padWithZeros(
|
||||
minutes,
|
||||
2,
|
||||
)}${padWithZeros(seconds, 2)}`
|
||||
}
|
||||
|
||||
function padWithZeros(vNumber: number, width: number) {
|
||||
@@ -145,12 +154,21 @@ function padWithZeros(vNumber: number, width: number) {
|
||||
|
||||
<template>
|
||||
<LoadingLayer v-if="isLoading" />
|
||||
<LogTerminal v-if="importTaskStarted" :api-url="importTaskApiURL" :req-params="importTaskParams" @back="importTaskDone()" />
|
||||
<LogTerminal
|
||||
v-if="importTaskStarted"
|
||||
:api-url="importTaskApiURL"
|
||||
:req-params="importTaskParams"
|
||||
@back="importTaskDone()"
|
||||
/>
|
||||
<div v-show="!importTaskStarted" class="atk-form">
|
||||
<div class="atk-label atk-data-file-label">Artrans {{ t('dataFile') }}</div>
|
||||
<FileUploader :api-url="uploadApiURL" @done="fileUploaded">
|
||||
<template v-slot:tip>
|
||||
使用「<a href="https://artalk.js.org/guide/transfer.html" target="_blank">转换工具</a>」将评论数据转为 Artrans 格式
|
||||
使用「
|
||||
<a href="https://artalk.js.org/guide/transfer.html" target="_blank">
|
||||
转换工具
|
||||
</a>
|
||||
」将评论数据转为 Artrans 格式
|
||||
</template>
|
||||
<template v-slot:done-msg>
|
||||
{{ t('uploadReadyToImport') }}
|
||||
@@ -175,12 +193,16 @@ function padWithZeros(vNumber: number, width: number) {
|
||||
<div class="atk-label">{{ t('payload') }} ({{ t('optional') }})</div>
|
||||
<textarea name="AtkPayload" v-model="importParams.payload"></textarea>
|
||||
<span class="atk-desc">
|
||||
参考「<a href="https://artalk.js.org/guide/transfer.html" target="_blank">文档 · 数据迁移</a>」
|
||||
参考「
|
||||
<a href="https://artalk.js.org/guide/transfer.html" target="_blank">
|
||||
文档 · 数据迁移
|
||||
</a>
|
||||
」
|
||||
</span>
|
||||
<button class="atk-btn" name="AtkSubmit" @click="startImportTask()">{{ t('import') }}</button>
|
||||
<button class="atk-btn" name="AtkSubmit" @click="startImportTask()">
|
||||
{{ t('import') }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
</style>
|
||||
<style scoped lang="scss"></style>
|
||||
@@ -17,14 +17,17 @@ const pagination = ref<InstanceType<typeof Pagination>>()
|
||||
const curtType = ref('all')
|
||||
|
||||
const addingUser = ref(false)
|
||||
const editingUser = ref<ArtalkType.UserDataForAdmin|undefined>()
|
||||
const editingUser = ref<ArtalkType.UserDataForAdmin | undefined>()
|
||||
|
||||
onMounted(() => {
|
||||
nav.updateTabs({
|
||||
'all': 'all',
|
||||
'admin': 'admin',
|
||||
'create': 'create'
|
||||
}, 'all')
|
||||
nav.updateTabs(
|
||||
{
|
||||
all: 'all',
|
||||
admin: 'admin',
|
||||
create: 'create',
|
||||
},
|
||||
'all',
|
||||
)
|
||||
|
||||
watch(curtTab, (tab) => {
|
||||
if (tab === 'create') {
|
||||
@@ -51,12 +54,15 @@ onMounted(() => {
|
||||
|
||||
function reqUsers(offset: number) {
|
||||
nav.setPageLoading(true)
|
||||
artalk?.ctx.getApi().user.userList(offset, pageSize.value, curtType.value as any)
|
||||
.then(got => {
|
||||
artalk?.ctx
|
||||
.getApi()
|
||||
.user.userList(offset, pageSize.value, curtType.value as any)
|
||||
.then((got) => {
|
||||
pageTotal.value = got.total
|
||||
users.value = got.users
|
||||
nav.scrollPageToTop()
|
||||
}).finally(() => {
|
||||
})
|
||||
.finally(() => {
|
||||
nav.setPageLoading(false)
|
||||
})
|
||||
}
|
||||
@@ -76,11 +82,11 @@ function editUser(user: ArtalkType.UserDataForAdmin) {
|
||||
}
|
||||
|
||||
function updateUser(user: ArtalkType.UserDataForAdmin) {
|
||||
const index = users.value.findIndex(u => u.id === user.id)
|
||||
const index = users.value.findIndex((u) => u.id === user.id)
|
||||
if (index != -1) {
|
||||
// 修改用户
|
||||
const orgUser = users.value[index]
|
||||
Object.keys(users).forEach(key => {
|
||||
Object.keys(users).forEach((key) => {
|
||||
;(orgUser as any)[key] = (users as any)[key]
|
||||
})
|
||||
} else {
|
||||
@@ -98,10 +104,16 @@ function closeEditUser() {
|
||||
}
|
||||
|
||||
function delUser(user: ArtalkType.UserDataForAdmin) {
|
||||
if (window.confirm(`该操作将删除 用户:"${user.name}" 邮箱:"${user.email}" 所有评论,包括其评论下面他人的回复评论,是否继续?`)) {
|
||||
artalk!.ctx.getApi().user.userDel(user.id)
|
||||
if (
|
||||
window.confirm(
|
||||
`该操作将删除 用户:"${user.name}" 邮箱:"${user.email}" 所有评论,包括其评论下面他人的回复评论,是否继续?`,
|
||||
)
|
||||
) {
|
||||
artalk!.ctx
|
||||
.getApi()
|
||||
.user.userDel(user.id)
|
||||
.then(() => {
|
||||
const index = users.value.findIndex(u => u.id === user.id)
|
||||
const index = users.value.findIndex((u) => u.id === user.id)
|
||||
users.value.splice(index, 1)
|
||||
|
||||
if (user.is_in_conf) {
|
||||
@@ -109,7 +121,7 @@ function delUser(user: ArtalkType.UserDataForAdmin) {
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
alert('删除失败:'+e.msg)
|
||||
alert('删除失败:' + e.msg)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -118,21 +130,39 @@ function delUser(user: ArtalkType.UserDataForAdmin) {
|
||||
<template>
|
||||
<div class="user-list-wrap">
|
||||
<div class="user-list">
|
||||
<div v-for="(user) in users" class="user-item">
|
||||
<div v-for="user in users" class="user-item">
|
||||
<div class="user-main">
|
||||
<div class="title">
|
||||
{{ user.name }}
|
||||
<span class="badge-grp">
|
||||
<span v-if="user.badge_name" class="badge" :style="{ backgroundColor: user.badge_color }">{{user.badge_name}}</span>
|
||||
<span v-else-if="user.is_admin" class="badge admin" :title="t('userAdminHint')">{{ t('Admin') }}</span>
|
||||
<span v-if="user.is_in_conf" class="badge in-conf" :title="t('userInConfHint')">{{ t('Config') }}</span>
|
||||
<span
|
||||
v-if="user.badge_name"
|
||||
class="badge"
|
||||
:style="{ backgroundColor: user.badge_color }"
|
||||
>
|
||||
{{ user.badge_name }}
|
||||
</span>
|
||||
<span
|
||||
v-else-if="user.is_admin"
|
||||
class="badge admin"
|
||||
:title="t('userAdminHint')"
|
||||
>
|
||||
{{ t('Admin') }}
|
||||
</span>
|
||||
<span
|
||||
v-if="user.is_in_conf"
|
||||
class="badge in-conf"
|
||||
:title="t('userInConfHint')"
|
||||
>
|
||||
{{ t('Config') }}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="sub">{{ user.email }}</div>
|
||||
</div>
|
||||
<div class="user-actions">
|
||||
<span @click="editUser(user)">{{ t('edit') }}</span>
|
||||
<span>{{ t('comment') }} ({{user.comment_count}})</span>
|
||||
<span>{{ t('comment') }} ({{ user.comment_count }})</span>
|
||||
<span @click="delUser(user)">{{ t('delete') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -188,7 +218,7 @@ function delUser(user: ArtalkType.UserDataForAdmin) {
|
||||
color: #fff;
|
||||
|
||||
&.admin {
|
||||
background: #0083FF;
|
||||
background: #0083ff;
|
||||
}
|
||||
|
||||
&.in-conf {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { defineStore } from 'pinia'
|
||||
import { artalk } from '../global'
|
||||
import type { ArtalkType } from 'artalk'
|
||||
|
||||
type TabsObj = {[name: string]: string}
|
||||
type TabsObj = { [name: string]: string }
|
||||
|
||||
export const useNavStore = defineStore('nav', () => {
|
||||
const curtTab = ref('')
|
||||
@@ -13,11 +13,11 @@ export const useNavStore = defineStore('nav', () => {
|
||||
const siteSwitcherShow = ref(false)
|
||||
|
||||
const isSearchEnabled = ref(false)
|
||||
const searchEvent = ref<((val: string) => void)|null>(null)
|
||||
const searchResetEvent = ref<(() => void)|null>(null)
|
||||
const searchEvent = ref<((val: string) => void) | null>(null)
|
||||
const searchResetEvent = ref<(() => void) | null>(null)
|
||||
|
||||
const isPageLoading = ref(false)
|
||||
const scrollableArea = ref<HTMLElement|null>(null)
|
||||
const scrollableArea = ref<HTMLElement | null>(null)
|
||||
|
||||
const updateTabs = (aTabs: TabsObj, activeTab?: string) => {
|
||||
tabs.value = aTabs
|
||||
@@ -53,23 +53,43 @@ export const useNavStore = defineStore('nav', () => {
|
||||
}
|
||||
|
||||
const refreshSites = () => {
|
||||
artalk?.ctx.getApi().site.siteGet().then((respSites) => {
|
||||
sites.value = respSites
|
||||
})
|
||||
artalk?.ctx
|
||||
.getApi()
|
||||
.site.siteGet()
|
||||
.then((respSites) => {
|
||||
sites.value = respSites
|
||||
})
|
||||
}
|
||||
|
||||
const enableSearch = (searchEvt: ((val: string) => void), searchResetEvt: () => void) => {
|
||||
const enableSearch = (
|
||||
searchEvt: (val: string) => void,
|
||||
searchResetEvt: () => void,
|
||||
) => {
|
||||
isSearchEnabled.value = true
|
||||
searchEvent.value = searchEvt
|
||||
searchResetEvent.value = searchResetEvt
|
||||
}
|
||||
|
||||
return {
|
||||
sites, curtPage, curtTab, tabs, siteSwitcherShow, scrollableArea, isPageLoading,
|
||||
updateTabs, setTabActive,
|
||||
showSiteSwitcher, hideSiteSwitcher, toggleSiteSwitcher,
|
||||
scrollPageToTop, scrollPageToEl, setPageLoading,
|
||||
sites,
|
||||
curtPage,
|
||||
curtTab,
|
||||
tabs,
|
||||
siteSwitcherShow,
|
||||
scrollableArea,
|
||||
isPageLoading,
|
||||
updateTabs,
|
||||
setTabActive,
|
||||
showSiteSwitcher,
|
||||
hideSiteSwitcher,
|
||||
toggleSiteSwitcher,
|
||||
scrollPageToTop,
|
||||
scrollPageToEl,
|
||||
setPageLoading,
|
||||
refreshSites,
|
||||
isSearchEnabled, searchEvent, searchResetEvent, enableSearch,
|
||||
isSearchEnabled,
|
||||
searchEvent,
|
||||
searchResetEvent,
|
||||
enableSearch,
|
||||
}
|
||||
})
|
||||
@@ -6,7 +6,8 @@ body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.artalk, .atk-layer-wrap {
|
||||
.artalk,
|
||||
.atk-layer-wrap {
|
||||
font-family: $font-family;
|
||||
}
|
||||
|
||||
@@ -59,7 +60,8 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
input, textarea {
|
||||
input,
|
||||
textarea {
|
||||
display: block;
|
||||
line-height: $form-height;
|
||||
padding: 0 10px;
|
||||
@@ -75,7 +77,7 @@ body {
|
||||
border-bottom-color: var(--at-color-bg-light);
|
||||
}
|
||||
|
||||
&[type="file"] {
|
||||
&[type='file'] {
|
||||
background: transparent;
|
||||
border: 2px solid var(--at-color-border);
|
||||
cursor: pointer;
|
||||
@@ -91,7 +93,7 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
&[type="file"]::-webkit-file-upload-button {
|
||||
&[type='file']::-webkit-file-upload-button {
|
||||
background: transparent;
|
||||
color: var(--at-color-font);
|
||||
border: 0;
|
||||
@@ -103,7 +105,7 @@ body {
|
||||
line-height: 25px;
|
||||
margin-right: 15px;
|
||||
cursor: pointer;
|
||||
transition: .2s ease;
|
||||
transition: 0.2s ease;
|
||||
-webkit-appearance: none;
|
||||
border-radius: 0;
|
||||
}
|
||||
@@ -151,7 +153,9 @@ body {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
input, select, button {
|
||||
input,
|
||||
select,
|
||||
button {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
@@ -185,7 +189,7 @@ body {
|
||||
font-size: 14px;
|
||||
|
||||
.atk-curt {
|
||||
color: var(--at-color-main)
|
||||
color: var(--at-color-main);
|
||||
}
|
||||
|
||||
.atk-abort {
|
||||
@@ -213,7 +217,8 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
.atk-log {}
|
||||
.atk-log {
|
||||
}
|
||||
|
||||
.atk-iframe {
|
||||
width: 100%;
|
||||
|
||||
@@ -7,15 +7,15 @@
|
||||
|
||||
&::-webkit-scrollbar-thumb,
|
||||
&::-webkit-scrollbar-thumb:window-inactive {
|
||||
background: rgba(86,86,86,.3);
|
||||
background: rgba(86, 86, 86, 0.3);
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb:vertical:hover {
|
||||
background: rgba(65,74,82,.77);
|
||||
background: rgba(65, 74, 82, 0.77);
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb:vertical:active {
|
||||
background: rgba(41,47,53,.77);
|
||||
background: rgba(41, 47, 53, 0.77);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
$font-family: "Helvetica Neue", "PingFangSC-Light", "Hiragino Sans GB", "Microsoft YaHei", "WenQuanYi Micro Hei", sans-serif;
|
||||
$font-family: 'Helvetica Neue', 'PingFangSC-Light', 'Hiragino Sans GB',
|
||||
'Microsoft YaHei', 'WenQuanYi Micro Hei', sans-serif;
|
||||
$font-size: 14px;
|
||||
$line-height: 1.6em;
|
||||
$block-radius: 6px;
|
||||
+71
-18
@@ -39,14 +39,49 @@ import type {
|
||||
|
||||
declare module 'vue-router/auto/routes' {
|
||||
export interface RouteNamedMap {
|
||||
'/': RouteRecordInfo<'/', '/', Record<never, never>, Record<never, never>>,
|
||||
'/comments': RouteRecordInfo<'/comments', '/comments', Record<never, never>, Record<never, never>>,
|
||||
'/login': RouteRecordInfo<'/login', '/login', Record<never, never>, Record<never, never>>,
|
||||
'/pages': RouteRecordInfo<'/pages', '/pages', Record<never, never>, Record<never, never>>,
|
||||
'/settings': RouteRecordInfo<'/settings', '/settings', Record<never, never>, Record<never, never>>,
|
||||
'/sites': RouteRecordInfo<'/sites', '/sites', Record<never, never>, Record<never, never>>,
|
||||
'/transfer': RouteRecordInfo<'/transfer', '/transfer', Record<never, never>, Record<never, never>>,
|
||||
'/users': RouteRecordInfo<'/users', '/users', Record<never, never>, Record<never, never>>,
|
||||
'/': RouteRecordInfo<'/', '/', Record<never, never>, Record<never, never>>
|
||||
'/comments': RouteRecordInfo<
|
||||
'/comments',
|
||||
'/comments',
|
||||
Record<never, never>,
|
||||
Record<never, never>
|
||||
>
|
||||
'/login': RouteRecordInfo<
|
||||
'/login',
|
||||
'/login',
|
||||
Record<never, never>,
|
||||
Record<never, never>
|
||||
>
|
||||
'/pages': RouteRecordInfo<
|
||||
'/pages',
|
||||
'/pages',
|
||||
Record<never, never>,
|
||||
Record<never, never>
|
||||
>
|
||||
'/settings': RouteRecordInfo<
|
||||
'/settings',
|
||||
'/settings',
|
||||
Record<never, never>,
|
||||
Record<never, never>
|
||||
>
|
||||
'/sites': RouteRecordInfo<
|
||||
'/sites',
|
||||
'/sites',
|
||||
Record<never, never>,
|
||||
Record<never, never>
|
||||
>
|
||||
'/transfer': RouteRecordInfo<
|
||||
'/transfer',
|
||||
'/transfer',
|
||||
Record<never, never>,
|
||||
Record<never, never>
|
||||
>
|
||||
'/users': RouteRecordInfo<
|
||||
'/users',
|
||||
'/users',
|
||||
Record<never, never>,
|
||||
Record<never, never>
|
||||
>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,29 +94,39 @@ declare module 'vue-router/auto' {
|
||||
* Type safe version of `RouteLocationNormalized` (the type of `to` and `from` in navigation guards).
|
||||
* Allows passing the name of the route to be passed as a generic.
|
||||
*/
|
||||
export type RouteLocationNormalized<Name extends keyof RouteNamedMap = keyof RouteNamedMap> = RouteLocationNormalizedTypedList<RouteNamedMap>[Name]
|
||||
export type RouteLocationNormalized<
|
||||
Name extends keyof RouteNamedMap = keyof RouteNamedMap,
|
||||
> = RouteLocationNormalizedTypedList<RouteNamedMap>[Name]
|
||||
|
||||
/**
|
||||
* Type safe version of `RouteLocationNormalizedLoaded` (the return type of `useRoute()`).
|
||||
* Allows passing the name of the route to be passed as a generic.
|
||||
*/
|
||||
export type RouteLocationNormalizedLoaded<Name extends keyof RouteNamedMap = keyof RouteNamedMap> = RouteLocationNormalizedLoadedTypedList<RouteNamedMap>[Name]
|
||||
export type RouteLocationNormalizedLoaded<
|
||||
Name extends keyof RouteNamedMap = keyof RouteNamedMap,
|
||||
> = RouteLocationNormalizedLoadedTypedList<RouteNamedMap>[Name]
|
||||
|
||||
/**
|
||||
* Type safe version of `RouteLocationResolved` (the returned route of `router.resolve()`).
|
||||
* Allows passing the name of the route to be passed as a generic.
|
||||
*/
|
||||
export type RouteLocationResolved<Name extends keyof RouteNamedMap = keyof RouteNamedMap> = RouteLocationResolvedTypedList<RouteNamedMap>[Name]
|
||||
export type RouteLocationResolved<
|
||||
Name extends keyof RouteNamedMap = keyof RouteNamedMap,
|
||||
> = RouteLocationResolvedTypedList<RouteNamedMap>[Name]
|
||||
|
||||
/**
|
||||
* Type safe version of `RouteLocation` . Allows passing the name of the route to be passed as a generic.
|
||||
*/
|
||||
export type RouteLocation<Name extends keyof RouteNamedMap = keyof RouteNamedMap> = RouteLocationTypedList<RouteNamedMap>[Name]
|
||||
export type RouteLocation<
|
||||
Name extends keyof RouteNamedMap = keyof RouteNamedMap,
|
||||
> = RouteLocationTypedList<RouteNamedMap>[Name]
|
||||
|
||||
/**
|
||||
* Type safe version of `RouteLocationRaw` . Allows passing the name of the route to be passed as a generic.
|
||||
*/
|
||||
export type RouteLocationRaw<Name extends keyof RouteNamedMap = keyof RouteNamedMap> =
|
||||
export type RouteLocationRaw<
|
||||
Name extends keyof RouteNamedMap = keyof RouteNamedMap,
|
||||
> =
|
||||
| RouteLocationAsString<RouteNamedMap>
|
||||
| RouteLocationAsRelativeTypedList<RouteNamedMap>[Name]
|
||||
| RouteLocationAsPathTypedList<RouteNamedMap>[Name]
|
||||
@@ -89,19 +134,27 @@ declare module 'vue-router/auto' {
|
||||
/**
|
||||
* Generate a type safe params for a route location. Requires the name of the route to be passed as a generic.
|
||||
*/
|
||||
export type RouteParams<Name extends keyof RouteNamedMap> = RouteNamedMap[Name]['params']
|
||||
export type RouteParams<Name extends keyof RouteNamedMap> =
|
||||
RouteNamedMap[Name]['params']
|
||||
/**
|
||||
* Generate a type safe raw params for a route location. Requires the name of the route to be passed as a generic.
|
||||
*/
|
||||
export type RouteParamsRaw<Name extends keyof RouteNamedMap> = RouteNamedMap[Name]['paramsRaw']
|
||||
export type RouteParamsRaw<Name extends keyof RouteNamedMap> =
|
||||
RouteNamedMap[Name]['paramsRaw']
|
||||
|
||||
export function useRouter(): RouterTyped
|
||||
export function useRoute<Name extends keyof RouteNamedMap = keyof RouteNamedMap>(name?: Name): RouteLocationNormalizedLoadedTypedList<RouteNamedMap>[Name]
|
||||
export function useRoute<
|
||||
Name extends keyof RouteNamedMap = keyof RouteNamedMap,
|
||||
>(name?: Name): RouteLocationNormalizedLoadedTypedList<RouteNamedMap>[Name]
|
||||
|
||||
export const useLink: UseLinkFnTyped<RouteNamedMap>
|
||||
|
||||
export function onBeforeRouteLeave(guard: NavigationGuard<RouteNamedMap>): void
|
||||
export function onBeforeRouteUpdate(guard: NavigationGuard<RouteNamedMap>): void
|
||||
export function onBeforeRouteLeave(
|
||||
guard: NavigationGuard<RouteNamedMap>,
|
||||
): void
|
||||
export function onBeforeRouteUpdate(
|
||||
guard: NavigationGuard<RouteNamedMap>,
|
||||
): void
|
||||
|
||||
export const RouterLink: RouterLinkTyped<RouteNamedMap>
|
||||
export const RouterLinkProps: RouterLinkPropsTyped<RouteNamedMap>
|
||||
|
||||
@@ -12,8 +12,8 @@ export default defineConfig({
|
||||
base: './',
|
||||
build: {
|
||||
target: 'es2015',
|
||||
outDir: resolve(__dirname, "dist"),
|
||||
minify: 'terser'
|
||||
outDir: resolve(__dirname, 'dist'),
|
||||
minify: 'terser',
|
||||
},
|
||||
plugins: [
|
||||
VueRouter({ importMode: 'sync' }),
|
||||
@@ -24,14 +24,17 @@ export default defineConfig({
|
||||
}),
|
||||
(() => ({
|
||||
name: 'prod-vue-resolver',
|
||||
resolveId (id) {
|
||||
resolveId(id) {
|
||||
// @issue https://github.com/vitejs/vite/issues/6607
|
||||
// dev mode vite resolves vue in other way
|
||||
// only in prod mode, `id === vue` is true
|
||||
if(id === 'vue') {
|
||||
return resolve(__dirname, './node_modules/vue/dist/vue.runtime.esm-bundler.js')
|
||||
if (id === 'vue') {
|
||||
return resolve(
|
||||
__dirname,
|
||||
'./node_modules/vue/dist/vue.runtime.esm-bundler.js',
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
}))(),
|
||||
],
|
||||
server: {
|
||||
@@ -40,8 +43,8 @@ export default defineConfig({
|
||||
css: {
|
||||
preprocessorOptions: {
|
||||
scss: {
|
||||
additionalData: `@import "./src/style/_variables.scss";@import "./src/style/_extends.scss";`
|
||||
},
|
||||
additionalData: `@import "./src/style/_variables.scss";@import "./src/style/_extends.scss";`,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
+283
-141
@@ -1,155 +1,297 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Artalk DEMO</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no, target-densitydpi=device-dpi" />
|
||||
<style>
|
||||
* { box-sizing: border-box; }
|
||||
body { padding:45px 10px;margin:0;background:#f4f4f4 }
|
||||
body.dark-mode { background:#000; }
|
||||
.card { margin:0 auto;max-width:700px;position:relative;overflow:hidden;background:#FFF;border-radius:4px;padding:20px;border: 1px solid transparent; }
|
||||
body.dark-mode .card { background: #1e2223;border: 1px solid #2d3235; }
|
||||
.card:not(:last-child) { margin-bottom:10px; }
|
||||
.card.billboard { text-align: center; }
|
||||
.title > a { display:block;text-decoration:none;color:#0083ff;font-size:35px;margin-top:0;margin-bottom:10px; }
|
||||
.title > a:hover { margin-left:0 }
|
||||
.title > a > span { opacity:0 }
|
||||
.title > a:hover>span { opacity:1 }
|
||||
.desc { font-size:12px;color:#697182 }
|
||||
body.dark-mode .desc { color:#848da3 }
|
||||
.social { margin-top:20px }
|
||||
.social > a:not(:last-child) { margin-right:2px }
|
||||
@media only screen and (max-width: 768px) {
|
||||
body { padding: 0;background: #FFF; }
|
||||
.card.billboard { padding-top: 20px; }
|
||||
.card { width: 100%;padding: 5px; }
|
||||
}
|
||||
.float-btn { user-select: none;bottom: 25px;display: block;cursor: pointer;text-align: center;width: 60px;height: 37px;line-height: 37px;font-size: 13px;color: #757e91;background: #fff;opacity: 0.8;border-radius: 4px;border: 1px solid #eceff2;position: fixed; }
|
||||
.to-night { z-index: 99999;left: 25px;background: #1e2223;color: #fff; }
|
||||
body.dark-mode .to-night { background: #006fd9;border: 1px solid #2d3235;color: #fff; }
|
||||
.to-top { right: 25px; }
|
||||
</style>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Artalk DEMO</title>
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no, target-densitydpi=device-dpi"
|
||||
/>
|
||||
<style>
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body {
|
||||
padding: 45px 10px;
|
||||
margin: 0;
|
||||
background: #f4f4f4;
|
||||
}
|
||||
body.dark-mode {
|
||||
background: #000;
|
||||
}
|
||||
.card {
|
||||
margin: 0 auto;
|
||||
max-width: 700px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
padding: 20px;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
body.dark-mode .card {
|
||||
background: #1e2223;
|
||||
border: 1px solid #2d3235;
|
||||
}
|
||||
.card:not(:last-child) {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.card.billboard {
|
||||
text-align: center;
|
||||
}
|
||||
.title > a {
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
color: #0083ff;
|
||||
font-size: 35px;
|
||||
margin-top: 0;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.title > a:hover {
|
||||
margin-left: 0;
|
||||
}
|
||||
.title > a > span {
|
||||
opacity: 0;
|
||||
}
|
||||
.title > a:hover > span {
|
||||
opacity: 1;
|
||||
}
|
||||
.desc {
|
||||
font-size: 12px;
|
||||
color: #697182;
|
||||
}
|
||||
body.dark-mode .desc {
|
||||
color: #848da3;
|
||||
}
|
||||
.social {
|
||||
margin-top: 20px;
|
||||
}
|
||||
.social > a:not(:last-child) {
|
||||
margin-right: 2px;
|
||||
}
|
||||
@media only screen and (max-width: 768px) {
|
||||
body {
|
||||
padding: 0;
|
||||
background: #fff;
|
||||
}
|
||||
.card.billboard {
|
||||
padding-top: 20px;
|
||||
}
|
||||
.card {
|
||||
width: 100%;
|
||||
padding: 5px;
|
||||
}
|
||||
}
|
||||
.float-btn {
|
||||
user-select: none;
|
||||
bottom: 25px;
|
||||
display: block;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
width: 60px;
|
||||
height: 37px;
|
||||
line-height: 37px;
|
||||
font-size: 13px;
|
||||
color: #757e91;
|
||||
background: #fff;
|
||||
opacity: 0.8;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #eceff2;
|
||||
position: fixed;
|
||||
}
|
||||
.to-night {
|
||||
z-index: 99999;
|
||||
left: 25px;
|
||||
background: #1e2223;
|
||||
color: #fff;
|
||||
}
|
||||
body.dark-mode .to-night {
|
||||
background: #006fd9;
|
||||
border: 1px solid #2d3235;
|
||||
color: #fff;
|
||||
}
|
||||
.to-top {
|
||||
right: 25px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- For mobile testing -->
|
||||
<script src="https://unpkg.com/vconsole@latest/dist/vconsole.min.js"></script>
|
||||
<script>
|
||||
// VConsole will be exported to `window.VConsole` by default.
|
||||
if (navigator.userAgentData.mobile) {
|
||||
var vConsole = new window.VConsole();
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="card billboard atk-fade-in">
|
||||
<div class="title"><a href="https://github.com/ArtalkJS/Artalk" target="_blank"><span>『 </span>Artalk<span> 』</span></a></div>
|
||||
<div class="desc">一款简洁的自托管评论系统 <span id="ArtalkPV"></span> | <span id="ArtalkCount"></span></div>
|
||||
<div class="social">
|
||||
<a href="https://www.npmjs.com/package/artalk" aria-label="npm package version" target="_blank"><img src="https://img.shields.io/npm/v/artalk.svg?style=flat-square"></a>
|
||||
<a href="https://www.npmjs.com/package/artalk" aria-label="npm package download number" target="_blank"><img src="https://img.shields.io/npm/dt/artalk.svg?style=flat-square"></a>
|
||||
<a href="https://github.com/ArtalkJS/Artalk/commits/master" aria-label="last commit" target="_blank"><img src="https://img.shields.io/github/last-commit/ArtalkJS/Artalk?style=flat-square"></a>
|
||||
<a href="https://github.com/ArtalkJS/Artalk/blob/master/LICENSE" aria-label="open source license" target="_blank"><img src="https://img.shields.io/github/license/ArtalkJS/Artalk.svg?style=flat-square"></a>
|
||||
<a href="https://github.com/ArtalkJS/Artalk" aria-label="GitHub star number" target="_blank"><img src="https://img.shields.io/github/stars/ArtalkJS/Artalk?style=flat-square"></a>
|
||||
<!-- For mobile testing -->
|
||||
<script src="https://unpkg.com/vconsole@latest/dist/vconsole.min.js"></script>
|
||||
<script>
|
||||
// VConsole will be exported to `window.VConsole` by default.
|
||||
if (navigator.userAgentData.mobile) {
|
||||
var vConsole = new window.VConsole()
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="card billboard atk-fade-in">
|
||||
<div class="title">
|
||||
<a href="https://github.com/ArtalkJS/Artalk" target="_blank">
|
||||
<span>『</span>
|
||||
Artalk
|
||||
<span>』</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="desc">
|
||||
一款简洁的自托管评论系统
|
||||
<span id="ArtalkPV"></span>
|
||||
|
|
||||
<span id="ArtalkCount"></span>
|
||||
</div>
|
||||
<div class="social">
|
||||
<a
|
||||
href="https://www.npmjs.com/package/artalk"
|
||||
aria-label="npm package version"
|
||||
target="_blank"
|
||||
>
|
||||
<img
|
||||
src="https://img.shields.io/npm/v/artalk.svg?style=flat-square"
|
||||
/>
|
||||
</a>
|
||||
<a
|
||||
href="https://www.npmjs.com/package/artalk"
|
||||
aria-label="npm package download number"
|
||||
target="_blank"
|
||||
>
|
||||
<img
|
||||
src="https://img.shields.io/npm/dt/artalk.svg?style=flat-square"
|
||||
/>
|
||||
</a>
|
||||
<a
|
||||
href="https://github.com/ArtalkJS/Artalk/commits/master"
|
||||
aria-label="last commit"
|
||||
target="_blank"
|
||||
>
|
||||
<img
|
||||
src="https://img.shields.io/github/last-commit/ArtalkJS/Artalk?style=flat-square"
|
||||
/>
|
||||
</a>
|
||||
<a
|
||||
href="https://github.com/ArtalkJS/Artalk/blob/master/LICENSE"
|
||||
aria-label="open source license"
|
||||
target="_blank"
|
||||
>
|
||||
<img
|
||||
src="https://img.shields.io/github/license/ArtalkJS/Artalk.svg?style=flat-square"
|
||||
/>
|
||||
</a>
|
||||
<a
|
||||
href="https://github.com/ArtalkJS/Artalk"
|
||||
aria-label="GitHub star number"
|
||||
target="_blank"
|
||||
>
|
||||
<img
|
||||
src="https://img.shields.io/github/stars/ArtalkJS/Artalk?style=flat-square"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card atk-fade-in">
|
||||
<div id="comments"></div>
|
||||
</div>
|
||||
<div class="float-btn to-night atk-fade-in">Blink</div>
|
||||
<div class="float-btn to-top atk-fade-in" style="display: none;">TOP</div>
|
||||
<div class="card atk-fade-in">
|
||||
<div id="comments"></div>
|
||||
</div>
|
||||
<div class="float-btn to-night atk-fade-in">Blink</div>
|
||||
<div class="float-btn to-top atk-fade-in" style="display: none">TOP</div>
|
||||
|
||||
<script type="module">
|
||||
import Artalk from './src/main'
|
||||
var artalk = Artalk.init({
|
||||
el: '#comments',
|
||||
pageKey: 'https://artalk.js.org/guide/intro.html',
|
||||
pageTitle: 'Artalk Home',
|
||||
server: (import.meta.env.DEV) ? 'http://localhost:23366' : 'https://artalk.qwqaq.com',
|
||||
site: 'ArtalkDocs',
|
||||
//useBackendConf: true,
|
||||
});
|
||||
|
||||
window.artalk = artalk;
|
||||
window.Artalk = Artalk;
|
||||
|
||||
;(function () {
|
||||
const toTopElem = document.querySelector('.to-top')
|
||||
toTopElem.addEventListener('click', () => {
|
||||
window.scrollTo(0, 0)
|
||||
<script type="module">
|
||||
import Artalk from './src/main'
|
||||
var artalk = Artalk.init({
|
||||
el: '#comments',
|
||||
pageKey: 'https://artalk.js.org/guide/intro.html',
|
||||
pageTitle: 'Artalk Home',
|
||||
server: import.meta.env.DEV
|
||||
? 'http://localhost:23366'
|
||||
: 'https://artalk.qwqaq.com',
|
||||
site: 'ArtalkDocs',
|
||||
//useBackendConf: true,
|
||||
})
|
||||
|
||||
checkShowToTop()
|
||||
document.addEventListener('scroll', () => {
|
||||
window.artalk = artalk
|
||||
window.Artalk = Artalk
|
||||
|
||||
;(function () {
|
||||
const toTopElem = document.querySelector('.to-top')
|
||||
toTopElem.addEventListener('click', () => {
|
||||
window.scrollTo(0, 0)
|
||||
})
|
||||
|
||||
checkShowToTop()
|
||||
})
|
||||
document.addEventListener('scroll', () => {
|
||||
checkShowToTop()
|
||||
})
|
||||
|
||||
function checkShowToTop() {
|
||||
const scrollTop = window.scrollY
|
||||
const whereShow = window.screen.height
|
||||
const isNeedShow = (scrollTop > 0) && (scrollTop > whereShow)
|
||||
function checkShowToTop() {
|
||||
const scrollTop = window.scrollY
|
||||
const whereShow = window.screen.height
|
||||
const isNeedShow = scrollTop > 0 && scrollTop > whereShow
|
||||
|
||||
if (isNeedShow) {
|
||||
toTopElem.style.display = ''
|
||||
} else {
|
||||
toTopElem.style.display = 'none'
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
;(function () {
|
||||
// @Refer: https://stackoverflow.com/questions/50840168/how-to-detect-if-the-os-is-in-dark-mode-in-browsers
|
||||
let darkMode = false
|
||||
|
||||
if (getLS() == "true") {
|
||||
darkMode = true;
|
||||
} else {
|
||||
try {
|
||||
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
||||
darkMode = true;
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function getLS() {
|
||||
return localStorage.getItem("dark_mode_on") || null
|
||||
}
|
||||
|
||||
function setLS(val) {
|
||||
return localStorage.setItem("dark_mode_on", String(val))
|
||||
}
|
||||
|
||||
function checkAndApplyDarkMode () {
|
||||
artalk.setDarkMode(darkMode)
|
||||
if (darkMode) {
|
||||
document.body.classList.add('dark-mode')
|
||||
} else {
|
||||
document.body.classList.remove('dark-mode')
|
||||
}
|
||||
}
|
||||
|
||||
checkAndApplyDarkMode()
|
||||
|
||||
const toNightElem = document.querySelector('.to-night')
|
||||
toNightElem.addEventListener('click', () => {
|
||||
darkMode = !darkMode
|
||||
setLS(darkMode)
|
||||
checkAndApplyDarkMode()
|
||||
})
|
||||
|
||||
// Listen
|
||||
try {
|
||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => {
|
||||
const newColorScheme = e.matches ? "dark" : "light";
|
||||
if (newColorScheme == "dark") {
|
||||
darkMode = true
|
||||
if (isNeedShow) {
|
||||
toTopElem.style.display = ''
|
||||
} else {
|
||||
darkMode = false
|
||||
toTopElem.style.display = 'none'
|
||||
}
|
||||
}
|
||||
})()
|
||||
|
||||
;(function () {
|
||||
// @Refer: https://stackoverflow.com/questions/50840168/how-to-detect-if-the-os-is-in-dark-mode-in-browsers
|
||||
let darkMode = false
|
||||
|
||||
if (getLS() == 'true') {
|
||||
darkMode = true
|
||||
} else {
|
||||
try {
|
||||
if (
|
||||
window.matchMedia &&
|
||||
window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
) {
|
||||
darkMode = true
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function getLS() {
|
||||
return localStorage.getItem('dark_mode_on') || null
|
||||
}
|
||||
|
||||
function setLS(val) {
|
||||
return localStorage.setItem('dark_mode_on', String(val))
|
||||
}
|
||||
|
||||
function checkAndApplyDarkMode() {
|
||||
artalk.setDarkMode(darkMode)
|
||||
if (darkMode) {
|
||||
document.body.classList.add('dark-mode')
|
||||
} else {
|
||||
document.body.classList.remove('dark-mode')
|
||||
}
|
||||
}
|
||||
|
||||
checkAndApplyDarkMode()
|
||||
|
||||
const toNightElem = document.querySelector('.to-night')
|
||||
toNightElem.addEventListener('click', () => {
|
||||
darkMode = !darkMode
|
||||
setLS(darkMode)
|
||||
checkAndApplyDarkMode()
|
||||
});
|
||||
} catch {}
|
||||
})()
|
||||
</script>
|
||||
</body>
|
||||
})
|
||||
|
||||
// Listen
|
||||
try {
|
||||
window
|
||||
.matchMedia('(prefers-color-scheme: dark)')
|
||||
.addEventListener('change', (e) => {
|
||||
const newColorScheme = e.matches ? 'dark' : 'light'
|
||||
if (newColorScheme == 'dark') {
|
||||
darkMode = true
|
||||
} else {
|
||||
darkMode = false
|
||||
}
|
||||
|
||||
checkAndApplyDarkMode()
|
||||
})
|
||||
} catch {}
|
||||
})()
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -34,14 +34,17 @@ export default defineConfig({
|
||||
},
|
||||
],
|
||||
// Run your local dev server before starting the tests.
|
||||
webServer: [{
|
||||
command: 'pnpm dev --port 5173',
|
||||
url: 'http://localhost:5173',
|
||||
reuseExistingServer: !process.env.CI,
|
||||
}, {
|
||||
cwd: '../../../', // ugly :(
|
||||
command: 'make dev',
|
||||
url: 'http://localhost:23366',
|
||||
reuseExistingServer: !process.env.CI,
|
||||
}]
|
||||
webServer: [
|
||||
{
|
||||
command: 'pnpm dev --port 5173',
|
||||
url: 'http://localhost:5173',
|
||||
reuseExistingServer: !process.env.CI,
|
||||
},
|
||||
{
|
||||
cwd: '../../../', // ugly :(
|
||||
command: 'make dev',
|
||||
url: 'http://localhost:23366',
|
||||
reuseExistingServer: !process.env.CI,
|
||||
},
|
||||
],
|
||||
})
|
||||
@@ -1,5 +1,5 @@
|
||||
import autoprefixer from 'autoprefixer'
|
||||
|
||||
export default {
|
||||
plugins: [ autoprefixer ]
|
||||
plugins: [autoprefixer],
|
||||
}
|
||||
@@ -12,12 +12,12 @@ fs.rmSync(outDir, { recursive: true, force: true })
|
||||
|
||||
const libraries = []
|
||||
|
||||
fs.readdirSync(i18nPath).forEach(f => {
|
||||
fs.readdirSync(i18nPath).forEach((f) => {
|
||||
if (['index.ts', 'external.ts'].includes(f)) return
|
||||
|
||||
const filename = path.join(i18nPath, f)
|
||||
const lang = path.parse(filename).name
|
||||
const content = fs.readFileSync(filename);
|
||||
const content = fs.readFileSync(filename)
|
||||
|
||||
// only compile the external locale
|
||||
if (!content.toString().includes('defineLocaleExternal')) return
|
||||
@@ -25,7 +25,8 @@ fs.readdirSync(i18nPath).forEach(f => {
|
||||
libraries.push({
|
||||
entry: filename,
|
||||
name: lang,
|
||||
fileName: (format) => ((format == "umd") ? `${lang}.js` : `${lang}.${format}.js`),
|
||||
fileName: (format) =>
|
||||
format == 'umd' ? `${lang}.js` : `${lang}.${format}.js`,
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -2,16 +2,18 @@ import { ApiOptions } from './_options'
|
||||
import { POST, Fetch } from './_request'
|
||||
|
||||
abstract class ApiBase {
|
||||
constructor(
|
||||
protected options: ApiOptions
|
||||
) {}
|
||||
constructor(protected options: ApiOptions) {}
|
||||
|
||||
protected async POST<T>(path: string, data?: {[key: string]: any}) {
|
||||
return POST<T>(this.options, this.options.baseURL+path, data)
|
||||
protected async POST<T>(path: string, data?: { [key: string]: any }) {
|
||||
return POST<T>(this.options, this.options.baseURL + path, data)
|
||||
}
|
||||
|
||||
protected async Fetch(path: string, init: RequestInit, timeout?: number): Promise<any> {
|
||||
return Fetch(this.options, this.options.baseURL+path, init, timeout)
|
||||
protected async Fetch(
|
||||
path: string,
|
||||
init: RequestInit,
|
||||
timeout?: number,
|
||||
): Promise<any> {
|
||||
return Fetch(this.options, this.options.baseURL + path, init, timeout)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,7 +4,12 @@ import { ApiOptions } from './_options'
|
||||
import $t from '../i18n'
|
||||
|
||||
/** 公共请求函数 */
|
||||
export async function Fetch(opts: ApiOptions, input: RequestInfo, init: RequestInit, timeout?: number): Promise<any> {
|
||||
export async function Fetch(
|
||||
opts: ApiOptions,
|
||||
input: RequestInfo,
|
||||
init: RequestInit,
|
||||
timeout?: number,
|
||||
): Promise<any> {
|
||||
// JWT
|
||||
if (opts.apiToken) {
|
||||
const headers = new Headers(init.headers) // 保留原有 headers
|
||||
@@ -31,28 +36,42 @@ export async function Fetch(opts: ApiOptions, input: RequestInfo, init: RequestI
|
||||
// 重新发起请求
|
||||
const recall = (resolve, reject) => {
|
||||
Fetch(opts, input, init)
|
||||
.then(d => { resolve(d) })
|
||||
.catch(e => { reject(e) })
|
||||
.then((d) => {
|
||||
resolve(d)
|
||||
})
|
||||
.catch((e) => {
|
||||
reject(e)
|
||||
})
|
||||
}
|
||||
|
||||
// 请求弹出层验证
|
||||
if (json.data?.need_captcha) {
|
||||
// 请求需要验证码
|
||||
json = await (new Promise<any>((resolve, reject) => {
|
||||
opts.onNeedCheckCaptcha && opts.onNeedCheckCaptcha({
|
||||
data: { imgData: json.data.img_data, iframe: json.data.iframe },
|
||||
recall: () => { recall(resolve, reject) },
|
||||
reject: () => { reject(json) }
|
||||
})
|
||||
}))
|
||||
json = await new Promise<any>((resolve, reject) => {
|
||||
opts.onNeedCheckCaptcha &&
|
||||
opts.onNeedCheckCaptcha({
|
||||
data: { imgData: json.data.img_data, iframe: json.data.iframe },
|
||||
recall: () => {
|
||||
recall(resolve, reject)
|
||||
},
|
||||
reject: () => {
|
||||
reject(json)
|
||||
},
|
||||
})
|
||||
})
|
||||
} else if (json.data?.need_login || isNoAccess) {
|
||||
// 请求需要管理员权限
|
||||
json = await (new Promise<any>((resolve, reject) => {
|
||||
opts.onNeedCheckAdmin && opts.onNeedCheckAdmin({
|
||||
recall: () => { recall(resolve, reject) },
|
||||
reject: () => { reject(json) }
|
||||
})
|
||||
}))
|
||||
json = await new Promise<any>((resolve, reject) => {
|
||||
opts.onNeedCheckAdmin &&
|
||||
opts.onNeedCheckAdmin({
|
||||
recall: () => {
|
||||
recall(resolve, reject)
|
||||
},
|
||||
reject: () => {
|
||||
reject(json)
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
if (!json.success) throw json // throw 相当于 reject(json)
|
||||
@@ -61,7 +80,11 @@ export async function Fetch(opts: ApiOptions, input: RequestInfo, init: RequestI
|
||||
}
|
||||
|
||||
/** 公共 POST 请求 */
|
||||
export async function POST<T>(opts: ApiOptions, url: string, data?: {[key: string]: any}) {
|
||||
export async function POST<T>(
|
||||
opts: ApiOptions,
|
||||
url: string,
|
||||
data?: { [key: string]: any },
|
||||
) {
|
||||
const init: RequestInit = {
|
||||
method: 'POST',
|
||||
}
|
||||
@@ -69,7 +92,7 @@ export async function POST<T>(opts: ApiOptions, url: string, data?: {[key: strin
|
||||
init.body = ToFormData(data)
|
||||
|
||||
const json = await Fetch(opts, url, init)
|
||||
return ((json.data || {}) as T)
|
||||
return (json.data || {}) as T
|
||||
}
|
||||
|
||||
/** 公共 GET 请求 (无 GET 方式 API) */
|
||||
@@ -81,9 +104,11 @@ export async function POST<T>(opts: ApiOptions, url: string, data?: {[key: strin
|
||||
// }
|
||||
|
||||
/** 对象转 FormData */
|
||||
export function ToFormData(object: {[key: string]: any}): FormData {
|
||||
export function ToFormData(object: { [key: string]: any }): FormData {
|
||||
const formData = new FormData()
|
||||
Object.keys(object).forEach(key => formData.append(key, String(object[key])))
|
||||
Object.keys(object).forEach((key) =>
|
||||
formData.append(key, String(object[key])),
|
||||
)
|
||||
return formData
|
||||
}
|
||||
|
||||
@@ -94,10 +119,12 @@ function timeoutFetch(url: RequestInfo, ms: number, opts: RequestInit) {
|
||||
let promise = fetch(url, { ...opts, signal: controller.signal })
|
||||
if (ms > 0) {
|
||||
const timer = setTimeout(() => controller.abort(), ms)
|
||||
promise.finally(() => { clearTimeout(timer) })
|
||||
promise.finally(() => {
|
||||
clearTimeout(timer)
|
||||
})
|
||||
}
|
||||
promise = promise.catch((err) => {
|
||||
throw ((err || {}).name === 'AbortError') ? new Error($t('reqAborted')) : err
|
||||
throw (err || {}).name === 'AbortError' ? new Error($t('reqAborted')) : err
|
||||
})
|
||||
return promise
|
||||
}
|
||||
@@ -7,7 +7,12 @@ import ApiBase from './_base'
|
||||
*/
|
||||
export default class CommentApi extends ApiBase {
|
||||
/** 评论 · 获取 */
|
||||
public get(offset: number, pageSize: number, flatMode?: boolean, paramsEditor?: (params: any) => void) {
|
||||
public get(
|
||||
offset: number,
|
||||
pageSize: number,
|
||||
flatMode?: boolean,
|
||||
paramsEditor?: (params: any) => void,
|
||||
) {
|
||||
const params: any = {
|
||||
page_key: this.options.pageKey,
|
||||
site_name: this.options.siteName,
|
||||
@@ -25,7 +30,16 @@ export default class CommentApi extends ApiBase {
|
||||
}
|
||||
|
||||
/** 评论 · 创建 */
|
||||
public async add(comment: { nick: string, email: string, link: string, content: string, rid: number, page_key: string, page_title?: string, site_name?: string }) {
|
||||
public async add(comment: {
|
||||
nick: string
|
||||
email: string
|
||||
link: string
|
||||
content: string
|
||||
rid: number
|
||||
page_key: string
|
||||
page_title?: string
|
||||
site_name?: string
|
||||
}) {
|
||||
const params: any = {
|
||||
name: comment.nick,
|
||||
email: comment.email,
|
||||
@@ -40,7 +54,7 @@ export default class CommentApi extends ApiBase {
|
||||
if (comment.site_name) params.site_name = comment.site_name
|
||||
|
||||
const data = await this.POST<any>('/add', params)
|
||||
return (data.comment as CommentData)
|
||||
return data.comment as CommentData
|
||||
}
|
||||
|
||||
/** 评论 · 修改 */
|
||||
@@ -50,7 +64,7 @@ export default class CommentApi extends ApiBase {
|
||||
}
|
||||
|
||||
const d = await this.POST<any>('/admin/comment-edit', params)
|
||||
return (d.comment as CommentData)
|
||||
return d.comment as CommentData
|
||||
}
|
||||
|
||||
/** 评论 · 删除 */
|
||||
@@ -73,6 +87,6 @@ export default class CommentApi extends ApiBase {
|
||||
this.withUserInfo(params)
|
||||
|
||||
const data = await this.POST<any>('/vote', params)
|
||||
return (data as {up: number, down: number})
|
||||
return data as { up: number; down: number }
|
||||
}
|
||||
}
|
||||
@@ -9,13 +9,18 @@ import admin from './admin'
|
||||
import upload from './upload'
|
||||
|
||||
const ApiComponents = {
|
||||
comment, page, site,
|
||||
user, system, captcha,
|
||||
admin, upload
|
||||
comment,
|
||||
page,
|
||||
site,
|
||||
user,
|
||||
system,
|
||||
captcha,
|
||||
admin,
|
||||
upload,
|
||||
}
|
||||
|
||||
class Api {
|
||||
constructor (opts: ApiOptions) {
|
||||
constructor(opts: ApiOptions) {
|
||||
Object.entries(ApiComponents).forEach(([key, ApiComponent]) => {
|
||||
this[key] = new ApiComponent(opts)
|
||||
})
|
||||
|
||||
@@ -14,7 +14,7 @@ export default class PageApi extends ApiBase {
|
||||
}
|
||||
|
||||
const d = await this.POST<any>('/admin/page-get', params)
|
||||
return (d as { pages: PageData[], total: number })
|
||||
return d as { pages: PageData[]; total: number }
|
||||
}
|
||||
|
||||
/** 页面 · 修改 */
|
||||
@@ -28,7 +28,7 @@ export default class PageApi extends ApiBase {
|
||||
}
|
||||
|
||||
const d = await this.POST<any>('/admin/page-edit', params)
|
||||
return (d.page as PageData)
|
||||
return d.page as PageData
|
||||
}
|
||||
|
||||
/** 页面 · 删除 */
|
||||
@@ -49,14 +49,14 @@ export default class PageApi extends ApiBase {
|
||||
if (getStatus) params.get_status = getStatus
|
||||
|
||||
const d = await this.POST<any>('/admin/page-fetch', params)
|
||||
return (d as any)
|
||||
return d as any
|
||||
}
|
||||
|
||||
/** PV */
|
||||
public async pv() {
|
||||
const params: any = {
|
||||
page_key: this.options.pageKey || '',
|
||||
page_title: this.options.pageTitle || ''
|
||||
page_title: this.options.pageTitle || '',
|
||||
}
|
||||
|
||||
const p = await this.POST<any>('/pv', params)
|
||||
@@ -65,19 +65,30 @@ export default class PageApi extends ApiBase {
|
||||
|
||||
/** 统计 */
|
||||
public async stat(
|
||||
type: 'latest_comments'|'latest_pages'|'pv_most_pages'|'comment_most_pages'|
|
||||
'page_pv'|'site_pv'|'page_comment'|'site_comment',
|
||||
pageKeys?: string|string[],
|
||||
limit?: number
|
||||
type:
|
||||
| 'latest_comments'
|
||||
| 'latest_pages'
|
||||
| 'pv_most_pages'
|
||||
| 'comment_most_pages'
|
||||
| 'page_pv'
|
||||
| 'site_pv'
|
||||
| 'page_comment'
|
||||
| 'site_comment',
|
||||
pageKeys?: string | string[],
|
||||
limit?: number,
|
||||
) {
|
||||
const params: any = {
|
||||
type,
|
||||
}
|
||||
|
||||
if (pageKeys) params.page_keys = Array.isArray(pageKeys) ? pageKeys.join(',') : pageKeys
|
||||
if (pageKeys)
|
||||
params.page_keys = Array.isArray(pageKeys) ? pageKeys.join(',') : pageKeys
|
||||
if (limit) params.limit = limit
|
||||
|
||||
const data = await this.POST<PageData[]|CommentData[]|object|number>(`/stat`, params)
|
||||
const data = await this.POST<PageData[] | CommentData[] | object | number>(
|
||||
`/stat`,
|
||||
params,
|
||||
)
|
||||
return data
|
||||
}
|
||||
}
|
||||
@@ -10,18 +10,19 @@ export default class SiteApi extends ApiBase {
|
||||
const params: any = {}
|
||||
|
||||
const d = await this.POST<any>('/admin/site-get', params)
|
||||
return (d.sites as SiteData[])
|
||||
return d.sites as SiteData[]
|
||||
}
|
||||
|
||||
/** 站点 · 创建 */
|
||||
public async siteAdd(name: string, urls: string) {
|
||||
const params: any = {
|
||||
name, urls,
|
||||
site_name: '' // 全局保留字段,当前站点名
|
||||
name,
|
||||
urls,
|
||||
site_name: '', // 全局保留字段,当前站点名
|
||||
}
|
||||
|
||||
const d = await this.POST<any>('/admin/site-add', params)
|
||||
return (d.site as SiteData)
|
||||
return d.site as SiteData
|
||||
}
|
||||
|
||||
/** 站点 · 修改 */
|
||||
@@ -33,7 +34,7 @@ export default class SiteApi extends ApiBase {
|
||||
}
|
||||
|
||||
const d = await this.POST<any>('/admin/site-edit', params)
|
||||
return (d.site as SiteData)
|
||||
return d.site as SiteData
|
||||
}
|
||||
|
||||
/** 站点 · 删除 */
|
||||
@@ -46,6 +47,6 @@ export default class SiteApi extends ApiBase {
|
||||
/** 导出 */
|
||||
public async export() {
|
||||
const d = await this.Fetch(`/admin/export`, { method: 'POST' }, 0)
|
||||
return (d.data?.data || '' as string)
|
||||
return d.data?.data || ('' as string)
|
||||
}
|
||||
}
|
||||
@@ -28,7 +28,9 @@ export default class SystemApi extends ApiBase {
|
||||
|
||||
/** 获取配置数据 */
|
||||
public async getSettings() {
|
||||
const data = await this.POST<{custom: string, template: string}>('/admin/setting-get')
|
||||
const data = await this.POST<{ custom: string; template: string }>(
|
||||
'/admin/setting-get',
|
||||
)
|
||||
return data
|
||||
}
|
||||
|
||||
@@ -42,8 +44,15 @@ export default class SystemApi extends ApiBase {
|
||||
|
||||
/** 获取 API 版本信息 */
|
||||
public async version() {
|
||||
const resp = await fetch(`${this.options.baseURL}/version`, { method: 'POST' })
|
||||
const resp = await fetch(`${this.options.baseURL}/version`, {
|
||||
method: 'POST',
|
||||
})
|
||||
const data = await resp.json()
|
||||
return data as { app: string, version: string, commit_hash: string, fe_min_version: string }
|
||||
return data as {
|
||||
app: string
|
||||
version: string
|
||||
commit_hash: string
|
||||
fe_min_version: string
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,10 +18,10 @@ export default class UploadApi extends ApiBase {
|
||||
|
||||
const init: RequestInit = {
|
||||
method: 'POST',
|
||||
body: form
|
||||
body: form,
|
||||
}
|
||||
|
||||
const json = await this.Fetch('/img-upload', init)
|
||||
return ((json.data || {}) as any) as { img_file: string, img_url: string }
|
||||
return (json.data || {}) as any as { img_file: string; img_url: string }
|
||||
}
|
||||
}
|
||||
@@ -9,18 +9,21 @@ export default class UserApi extends ApiBase {
|
||||
/** 用户 · 登录 */
|
||||
public async login(name: string, email: string, password: string) {
|
||||
const params: any = {
|
||||
name, email, password
|
||||
name,
|
||||
email,
|
||||
password,
|
||||
}
|
||||
|
||||
const data = await this.POST<any>('/login', params)
|
||||
return data as { token: string, user: UserData }
|
||||
return data as { token: string; user: UserData }
|
||||
}
|
||||
|
||||
/** 用户 · 获取 */
|
||||
public userGet(name: string, email: string) {
|
||||
const ctrl = new AbortController()
|
||||
const params: any = {
|
||||
name, email
|
||||
name,
|
||||
email,
|
||||
}
|
||||
|
||||
const req = this.Fetch(`/user-get`, {
|
||||
@@ -28,7 +31,7 @@ export default class UserApi extends ApiBase {
|
||||
body: ToFormData(params),
|
||||
signal: ctrl.signal,
|
||||
}).then((json) => ({
|
||||
user: json.data.user as UserData|null,
|
||||
user: json.data.user as UserData | null,
|
||||
is_login: json.data.is_login as boolean,
|
||||
unread: (json.data.unread || []) as NotifyData[],
|
||||
unread_count: json.data.unread_count || 0,
|
||||
@@ -36,14 +39,19 @@ export default class UserApi extends ApiBase {
|
||||
|
||||
return {
|
||||
req,
|
||||
abort: () => { ctrl.abort() },
|
||||
abort: () => {
|
||||
ctrl.abort()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/** 用户 · 登录状态 */
|
||||
public async loginStatus() {
|
||||
const data = await this.POST<any>('/login-status', this.withUserInfo({}))
|
||||
return (data || { is_login: false, is_admin: false }) as { is_login: boolean, is_admin: boolean }
|
||||
return (data || { is_login: false, is_admin: false }) as {
|
||||
is_login: boolean
|
||||
is_admin: boolean
|
||||
}
|
||||
}
|
||||
|
||||
/** 用户 · 注销 */
|
||||
@@ -69,7 +77,11 @@ export default class UserApi extends ApiBase {
|
||||
}
|
||||
|
||||
/** 用户 · 列表 */
|
||||
public async userList(offset?: number, limit?: number, type?: 'all'|'admin'|'in_conf') {
|
||||
public async userList(
|
||||
offset?: number,
|
||||
limit?: number,
|
||||
type?: 'all' | 'admin' | 'in_conf',
|
||||
) {
|
||||
const params: any = {
|
||||
offset: offset || 0,
|
||||
limit: limit || 15,
|
||||
@@ -78,7 +90,7 @@ export default class UserApi extends ApiBase {
|
||||
if (type) params.type = type
|
||||
|
||||
const d = await this.POST<any>('/admin/user-get', params)
|
||||
return (d as { users: UserDataForAdmin[], total: number })
|
||||
return d as { users: UserDataForAdmin[]; total: number }
|
||||
}
|
||||
|
||||
/** 用户 · 新增 */
|
||||
@@ -96,7 +108,7 @@ export default class UserApi extends ApiBase {
|
||||
}
|
||||
|
||||
const d = await this.POST<any>('/admin/user-add', params)
|
||||
return (d.user as UserDataForAdmin)
|
||||
return d.user as UserDataForAdmin
|
||||
}
|
||||
|
||||
/** 用户 · 修改 */
|
||||
@@ -115,13 +127,13 @@ export default class UserApi extends ApiBase {
|
||||
}
|
||||
|
||||
const d = await this.POST<any>('/admin/user-edit', params)
|
||||
return (d.user as UserDataForAdmin)
|
||||
return d.user as UserDataForAdmin
|
||||
}
|
||||
|
||||
/** 用户 · 删除 */
|
||||
public userDel(userID: number) {
|
||||
return this.POST('/admin/user-del', {
|
||||
id: String(userID)
|
||||
id: String(userID),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,11 @@
|
||||
import './style/main.scss'
|
||||
|
||||
import type { ArtalkConfig, EventPayloadMap, ArtalkPlugin, ContextApi } from '~/types'
|
||||
import type {
|
||||
ArtalkConfig,
|
||||
EventPayloadMap,
|
||||
ArtalkPlugin,
|
||||
ContextApi,
|
||||
} from '~/types'
|
||||
import type { EventHandler } from './lib/event-manager'
|
||||
import Context from './context'
|
||||
import { handelCustomConf, convertApiOptions } from './config'
|
||||
@@ -11,7 +16,7 @@ import Api from './api'
|
||||
import type { TInjectedServices } from './service'
|
||||
|
||||
/** Global Plugins for all instances */
|
||||
const GlobalPlugins: ArtalkPlugin[] = [ ...DefaultPlugins ]
|
||||
const GlobalPlugins: ArtalkPlugin[] = [...DefaultPlugins]
|
||||
|
||||
/**
|
||||
* Artalk
|
||||
@@ -24,7 +29,7 @@ export default class Artalk {
|
||||
public $root!: HTMLElement
|
||||
|
||||
/** Plugins */
|
||||
protected plugins: ArtalkPlugin[] = [ ...GlobalPlugins ]
|
||||
protected plugins: ArtalkPlugin[] = [...GlobalPlugins]
|
||||
|
||||
constructor(conf: Partial<ArtalkConfig>) {
|
||||
// Init Config
|
||||
@@ -41,7 +46,7 @@ export default class Artalk {
|
||||
})
|
||||
|
||||
// Init Plugins
|
||||
this.plugins.forEach(plugin => {
|
||||
this.plugins.forEach((plugin) => {
|
||||
if (typeof plugin === 'function') plugin(this.ctx)
|
||||
})
|
||||
|
||||
@@ -67,17 +72,26 @@ export default class Artalk {
|
||||
}
|
||||
|
||||
/** Add an event listener */
|
||||
public on<K extends keyof EventPayloadMap>(name: K, handler: EventHandler<EventPayloadMap[K]>) {
|
||||
public on<K extends keyof EventPayloadMap>(
|
||||
name: K,
|
||||
handler: EventHandler<EventPayloadMap[K]>,
|
||||
) {
|
||||
this.ctx.on(name, handler)
|
||||
}
|
||||
|
||||
/** Remove an event listener */
|
||||
public off<K extends keyof EventPayloadMap>(name: K, handler: EventHandler<EventPayloadMap[K]>) {
|
||||
public off<K extends keyof EventPayloadMap>(
|
||||
name: K,
|
||||
handler: EventHandler<EventPayloadMap[K]>,
|
||||
) {
|
||||
this.ctx.off(name, handler)
|
||||
}
|
||||
|
||||
/** Trigger an event */
|
||||
public trigger<K extends keyof EventPayloadMap>(name: K, payload?: EventPayloadMap[K]) {
|
||||
public trigger<K extends keyof EventPayloadMap>(
|
||||
name: K,
|
||||
payload?: EventPayloadMap[K],
|
||||
) {
|
||||
this.ctx.trigger(name, payload)
|
||||
}
|
||||
|
||||
@@ -110,7 +124,7 @@ export default class Artalk {
|
||||
pageKey: conf.pageKey,
|
||||
countEl: conf.countEl,
|
||||
pvEl: conf.pvEl,
|
||||
pvAdd: false
|
||||
pvAdd: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -4,33 +4,47 @@ import ActionBtn from '../components/action-btn'
|
||||
export default class CommentActions {
|
||||
private comment: Comment
|
||||
|
||||
private get ctx() { return this.comment.ctx }
|
||||
private get data() { return this.comment.getData() }
|
||||
private get cConf() { return this.comment.getConf() }
|
||||
private get ctx() {
|
||||
return this.comment.ctx
|
||||
}
|
||||
private get data() {
|
||||
return this.comment.getData()
|
||||
}
|
||||
private get cConf() {
|
||||
return this.comment.getConf()
|
||||
}
|
||||
|
||||
public constructor(comment: Comment) {
|
||||
this.comment = comment
|
||||
}
|
||||
|
||||
/** 投票操作 */
|
||||
public vote(type: 'up'|'down') {
|
||||
const actionBtn = (type === 'up') ? this.comment.getRender().voteBtnUp : this.comment.getRender().voteBtnDown
|
||||
public vote(type: 'up' | 'down') {
|
||||
const actionBtn =
|
||||
type === 'up'
|
||||
? this.comment.getRender().voteBtnUp
|
||||
: this.comment.getRender().voteBtnDown
|
||||
|
||||
this.ctx.getApi().comment.vote(this.data.id, `comment_${type}`)
|
||||
.then((v) => {
|
||||
this.data.vote_up = v.up
|
||||
this.data.vote_down = v.down
|
||||
this.comment.getRender().voteBtnUp?.updateText()
|
||||
this.comment.getRender().voteBtnDown?.updateText()
|
||||
})
|
||||
.catch((err) => {
|
||||
actionBtn?.setError(this.ctx.$t('voteFail'))
|
||||
console.log(err)
|
||||
})
|
||||
this.ctx
|
||||
.getApi()
|
||||
.comment.vote(this.data.id, `comment_${type}`)
|
||||
.then((v) => {
|
||||
this.data.vote_up = v.up
|
||||
this.data.vote_down = v.down
|
||||
this.comment.getRender().voteBtnUp?.updateText()
|
||||
this.comment.getRender().voteBtnDown?.updateText()
|
||||
})
|
||||
.catch((err) => {
|
||||
actionBtn?.setError(this.ctx.$t('voteFail'))
|
||||
console.log(err)
|
||||
})
|
||||
}
|
||||
|
||||
/** 管理员 - 评论状态修改 */
|
||||
public adminEdit(type: 'collapsed'|'pending'|'pinned', btnElem: ActionBtn) {
|
||||
public adminEdit(
|
||||
type: 'collapsed' | 'pending' | 'pinned',
|
||||
btnElem: ActionBtn,
|
||||
) {
|
||||
if (btnElem.isLoading) return // 若正在修改中
|
||||
|
||||
btnElem.setLoading(true, `${this.ctx.$t('editing')}...`)
|
||||
@@ -45,15 +59,19 @@ export default class CommentActions {
|
||||
modify.is_pinned = !modify.is_pinned
|
||||
}
|
||||
|
||||
this.ctx.getApi().comment.commentEdit(modify).then((data) => {
|
||||
btnElem.setLoading(false)
|
||||
this.ctx
|
||||
.getApi()
|
||||
.comment.commentEdit(modify)
|
||||
.then((data) => {
|
||||
btnElem.setLoading(false)
|
||||
|
||||
// 刷新当前 Comment UI
|
||||
this.comment.setData(data)
|
||||
}).catch((err) => {
|
||||
console.error(err)
|
||||
btnElem.setError(this.ctx.$t('editFail'))
|
||||
})
|
||||
// 刷新当前 Comment UI
|
||||
this.comment.setData(data)
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err)
|
||||
btnElem.setError(this.ctx.$t('editFail'))
|
||||
})
|
||||
}
|
||||
|
||||
/** 管理员 - 评论删除 */
|
||||
@@ -61,7 +79,9 @@ export default class CommentActions {
|
||||
if (btnElem.isLoading) return // 若正在删除中
|
||||
|
||||
btnElem.setLoading(true, `${this.ctx.$t('deleting')}...`)
|
||||
this.ctx.getApi().comment.commentDel(this.data.id, this.data.site_name)
|
||||
this.ctx
|
||||
.getApi()
|
||||
.comment.commentDel(this.data.id, this.data.site_name)
|
||||
.then(() => {
|
||||
btnElem.setLoading(false)
|
||||
if (this.cConf.onDelete) this.cConf.onDelete(this.comment)
|
||||
|
||||
@@ -24,11 +24,11 @@ export default class Comment extends Component {
|
||||
private data: CommentData
|
||||
private cConf: CommentConf
|
||||
|
||||
private parent: Comment|null
|
||||
private parent: Comment | null
|
||||
private children: Comment[] = []
|
||||
|
||||
private nestCurt: number // 当前嵌套层数
|
||||
private nestMax: number // 最大嵌套层数
|
||||
private nestMax: number // 最大嵌套层数
|
||||
|
||||
constructor(ctx: ContextApi, data: CommentData, conf: CommentConf) {
|
||||
super(ctx)
|
||||
@@ -106,7 +106,10 @@ export default class Comment extends Component {
|
||||
}
|
||||
|
||||
/** 置入子评论 */
|
||||
public putChild(childC: Comment, insertMode: 'append'|'prepend' = 'append') {
|
||||
public putChild(
|
||||
childC: Comment,
|
||||
insertMode: 'append' | 'prepend' = 'append',
|
||||
) {
|
||||
childC.parent = this
|
||||
childC.nestCurt = this.nestCurt + 1 // 嵌套层数 +1
|
||||
|
||||
@@ -181,7 +184,7 @@ export default class Comment extends Component {
|
||||
const info = UADetect(this.data.ua)
|
||||
return {
|
||||
browser: `${info.browser} ${info.version}`,
|
||||
os: `${info.os} ${info.osVersion}`
|
||||
os: `${info.os} ${info.osVersion}`,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ export interface IHeightLimitConf {
|
||||
|
||||
export interface IHeightLimitRule {
|
||||
/** Target element need to check */
|
||||
el: HTMLElement|null|undefined
|
||||
el: HTMLElement | null | undefined
|
||||
|
||||
/** Max height (unit: px) */
|
||||
max: number
|
||||
@@ -23,15 +23,16 @@ export type THeightLimitRuleSet = IHeightLimitRule[]
|
||||
|
||||
/** Check all elements below the max height limit */
|
||||
export function check(conf: IHeightLimitConf, rules: THeightLimitRuleSet) {
|
||||
rules.forEach(({
|
||||
el, max: maxHeight, imgContains
|
||||
}) => {
|
||||
rules.forEach(({ el, max: maxHeight, imgContains }) => {
|
||||
const _apply = () => {
|
||||
if (!el) return
|
||||
if (!conf.scrollable)
|
||||
applyHeightLimit({ el, maxHeight, postBtnClick: conf.postExpandBtnClick })
|
||||
else
|
||||
applyScrollableHeightLimit({ el, maxHeight })
|
||||
applyHeightLimit({
|
||||
el,
|
||||
maxHeight,
|
||||
postBtnClick: conf.postExpandBtnClick,
|
||||
})
|
||||
else applyScrollableHeightLimit({ el, maxHeight })
|
||||
}
|
||||
|
||||
// checking
|
||||
@@ -39,7 +40,8 @@ export function check(conf: IHeightLimitConf, rules: THeightLimitRuleSet) {
|
||||
if (el && Utils.getHeight(el) > maxHeight) _apply() // 是否超过高度
|
||||
}
|
||||
_check() // check now
|
||||
if (imgContains && el) // check again if img contains
|
||||
if (imgContains && el)
|
||||
// check again if img contains
|
||||
Utils.onImagesLoaded(el, () => _check())
|
||||
})
|
||||
}
|
||||
@@ -49,8 +51,8 @@ const HEIGHT_LIMIT_CSS = 'atk-height-limit'
|
||||
|
||||
/** Apply height limit on an element and add expand btn */
|
||||
export function applyHeightLimit(obj: {
|
||||
el: HTMLElement,
|
||||
maxHeight: number,
|
||||
el: HTMLElement
|
||||
maxHeight: number
|
||||
postBtnClick?: (e: MouseEvent) => void
|
||||
}) {
|
||||
if (!obj.el) return
|
||||
@@ -62,7 +64,9 @@ export function applyHeightLimit(obj: {
|
||||
obj.el.style.overflow = 'hidden'
|
||||
|
||||
/* Expand button */
|
||||
const $expandBtn = Utils.createElement(`<div class="atk-height-limit-btn">${$t('readMore')}</span>`)
|
||||
const $expandBtn = Utils.createElement(
|
||||
`<div class="atk-height-limit-btn">${$t('readMore')}</span>`,
|
||||
)
|
||||
$expandBtn.onclick = (e) => {
|
||||
e.stopPropagation()
|
||||
disposeHeightLimit(obj.el)
|
||||
@@ -90,7 +94,7 @@ const HEIGHT_LIMIT_SCROLL_CSS = 'atk-height-limit-scroll'
|
||||
|
||||
/** Apply scrollable height limit */
|
||||
export function applyScrollableHeightLimit(obj: {
|
||||
el: HTMLElement,
|
||||
el: HTMLElement
|
||||
maxHeight: number
|
||||
}) {
|
||||
if (!obj.el) return
|
||||
|
||||
@@ -10,10 +10,18 @@ import loadRenders from './renders'
|
||||
export default class Render {
|
||||
public comment: Comment
|
||||
|
||||
public get ctx() { return this.comment.ctx }
|
||||
public get data() { return this.comment.getData() }
|
||||
public get conf() { return this.comment.conf }
|
||||
public get cConf() { return this.comment.getConf() }
|
||||
public get ctx() {
|
||||
return this.comment.ctx
|
||||
}
|
||||
public get data() {
|
||||
return this.comment.getData()
|
||||
}
|
||||
public get conf() {
|
||||
return this.comment.conf
|
||||
}
|
||||
public get cConf() {
|
||||
return this.comment.getConf()
|
||||
}
|
||||
|
||||
public $el!: HTMLElement
|
||||
public $main!: HTMLElement
|
||||
@@ -22,7 +30,7 @@ export default class Render {
|
||||
public $headerBadgeWrap!: HTMLElement
|
||||
public $body!: HTMLElement
|
||||
public $content!: HTMLElement
|
||||
public $childrenWrap!: HTMLElement|null
|
||||
public $childrenWrap!: HTMLElement | null
|
||||
public $actions!: HTMLElement
|
||||
public voteBtnUp?: ActionBtn
|
||||
public voteBtnDown?: ActionBtn
|
||||
@@ -62,20 +70,24 @@ export default class Render {
|
||||
const contentMaxH = conf.content
|
||||
const childrenMaxH = conf.children
|
||||
|
||||
HeightLimit.check({
|
||||
postExpandBtnClick: () => {
|
||||
// 子评论数仅有 1,直接取消限高
|
||||
const children = this.comment.getChildren()
|
||||
if (children.length === 1) HeightLimit.disposeHeightLimit(children[0].getRender().$content)
|
||||
HeightLimit.check(
|
||||
{
|
||||
postExpandBtnClick: () => {
|
||||
// 子评论数仅有 1,直接取消限高
|
||||
const children = this.comment.getChildren()
|
||||
if (children.length === 1)
|
||||
HeightLimit.disposeHeightLimit(children[0].getRender().$content)
|
||||
},
|
||||
scrollable: conf.scrollable,
|
||||
},
|
||||
scrollable: conf.scrollable
|
||||
}, [
|
||||
// 评论内容限高
|
||||
{ el: this.$content, max: contentMaxH, imgContains: true },
|
||||
{ el: this.$replyTo, max: contentMaxH, imgContains: true },
|
||||
// 子评论区域限高(仅嵌套模式)
|
||||
{ el: this.$childrenWrap, max: childrenMaxH, imgContains: false }
|
||||
])
|
||||
[
|
||||
// 评论内容限高
|
||||
{ el: this.$content, max: contentMaxH, imgContains: true },
|
||||
{ el: this.$replyTo, max: contentMaxH, imgContains: true },
|
||||
// 子评论区域限高(仅嵌套模式)
|
||||
{ el: this.$childrenWrap, max: childrenMaxH, imgContains: false },
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
/** 子评论区域移除限高 */
|
||||
@@ -102,7 +114,9 @@ export default class Render {
|
||||
/** 初始化子评论区域 Wrap */
|
||||
public renderChildrenWrap() {
|
||||
if (!this.$childrenWrap) {
|
||||
this.$childrenWrap = Utils.createElement('<div class="atk-comment-children"></div>')
|
||||
this.$childrenWrap = Utils.createElement(
|
||||
'<div class="atk-comment-children"></div>',
|
||||
)
|
||||
this.$main.append(this.$childrenWrap)
|
||||
}
|
||||
|
||||
|
||||
@@ -7,28 +7,36 @@ import type Render from '../render'
|
||||
*/
|
||||
export default function renderActions(r: Render) {
|
||||
Object.entries({
|
||||
renderVote, renderReply,
|
||||
renderVote,
|
||||
renderReply,
|
||||
// 管理员操作
|
||||
renderCollapse, renderModerator, renderPin, renderEdit, renderDel
|
||||
renderCollapse,
|
||||
renderModerator,
|
||||
renderPin,
|
||||
renderEdit,
|
||||
renderDel,
|
||||
}).forEach(([name, render]) => {
|
||||
render(r)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// 操作按钮 - 投票
|
||||
function renderVote(r: Render) {
|
||||
if (!r.ctx.conf.vote) return // 关闭投票功能
|
||||
|
||||
// 赞同按钮
|
||||
r.voteBtnUp = new ActionBtn(() => `${r.ctx.$t('voteUp')} (${r.data.vote_up || 0})`).appendTo(r.$actions)
|
||||
r.voteBtnUp = new ActionBtn(
|
||||
() => `${r.ctx.$t('voteUp')} (${r.data.vote_up || 0})`,
|
||||
).appendTo(r.$actions)
|
||||
r.voteBtnUp.setClick(() => {
|
||||
r.comment.getActions().vote('up')
|
||||
})
|
||||
|
||||
// 反对按钮
|
||||
if (r.ctx.conf.voteDown) {
|
||||
r.voteBtnDown = new ActionBtn(() => `${r.ctx.$t('voteDown')} (${r.data.vote_down || 0})`).appendTo(r.$actions)
|
||||
r.voteBtnDown = new ActionBtn(
|
||||
() => `${r.ctx.$t('voteDown')} (${r.data.vote_down || 0})`,
|
||||
).appendTo(r.$actions)
|
||||
r.voteBtnDown.setClick(() => {
|
||||
r.comment.getActions().vote('down')
|
||||
})
|
||||
@@ -54,8 +62,9 @@ function renderReply(r: Render) {
|
||||
// 操作按钮 - 折叠
|
||||
function renderCollapse(r: Render) {
|
||||
const collapseBtn = new ActionBtn({
|
||||
text: () => (r.data.is_collapsed ? r.ctx.$t('expand') : r.ctx.$t('collapse')),
|
||||
adminOnly: true
|
||||
text: () =>
|
||||
r.data.is_collapsed ? r.ctx.$t('expand') : r.ctx.$t('collapse'),
|
||||
adminOnly: true,
|
||||
})
|
||||
collapseBtn.appendTo(r.$actions)
|
||||
collapseBtn.setClick(() => {
|
||||
@@ -66,8 +75,9 @@ function renderCollapse(r: Render) {
|
||||
// 操作按钮 - 审核
|
||||
function renderModerator(r: Render) {
|
||||
const pendingBtn = new ActionBtn({
|
||||
text: () => (r.data.is_pending ? r.ctx.$t('pending') : r.ctx.$t('approved')),
|
||||
adminOnly: true
|
||||
text: () =>
|
||||
r.data.is_pending ? r.ctx.$t('pending') : r.ctx.$t('approved'),
|
||||
adminOnly: true,
|
||||
})
|
||||
pendingBtn.appendTo(r.$actions)
|
||||
pendingBtn.setClick(() => {
|
||||
@@ -79,7 +89,7 @@ function renderModerator(r: Render) {
|
||||
function renderPin(r: Render) {
|
||||
const pinnedBtn = new ActionBtn({
|
||||
text: () => (r.data.is_pinned ? r.ctx.$t('unpin') : r.ctx.$t('pin')),
|
||||
adminOnly: true
|
||||
adminOnly: true,
|
||||
})
|
||||
pinnedBtn.appendTo(r.$actions)
|
||||
pinnedBtn.setClick(() => {
|
||||
@@ -91,7 +101,7 @@ function renderPin(r: Render) {
|
||||
function renderEdit(r: Render) {
|
||||
const editBtn = new ActionBtn({
|
||||
text: r.ctx.$t('edit'),
|
||||
adminOnly: true
|
||||
adminOnly: true,
|
||||
})
|
||||
editBtn.appendTo(r.$actions)
|
||||
editBtn.setClick(() => {
|
||||
|
||||
@@ -9,11 +9,17 @@ export default function renderAvatar(r: Render) {
|
||||
const $avatarImg = Utils.createElement<HTMLImageElement>('<img />')
|
||||
|
||||
const avatarURLBuilder = r.conf.avatarURLBuilder
|
||||
$avatarImg.src = avatarURLBuilder ? avatarURLBuilder(r.data) : r.comment.getGravatarURL()
|
||||
$avatarImg.src = avatarURLBuilder
|
||||
? avatarURLBuilder(r.data)
|
||||
: r.comment.getGravatarURL()
|
||||
|
||||
if (r.data.link) {
|
||||
const $avatarA = Utils.createElement<HTMLLinkElement>('<a target="_blank" rel="noreferrer noopener nofollow"></a>')
|
||||
$avatarA.href = Utils.isValidURL(r.data.link) ? r.data.link : `https://${r.data.link}`
|
||||
const $avatarA = Utils.createElement<HTMLLinkElement>(
|
||||
'<a target="_blank" rel="noreferrer noopener nofollow"></a>',
|
||||
)
|
||||
$avatarA.href = Utils.isValidURL(r.data.link)
|
||||
? r.data.link
|
||||
: `https://${r.data.link}`
|
||||
$avatarA.append($avatarImg)
|
||||
$avatar.append($avatarA)
|
||||
} else {
|
||||
|
||||
@@ -21,7 +21,8 @@ export default function renderContent(r: Render) {
|
||||
</div>`)
|
||||
r.$body.insertAdjacentElement('beforeend', collapsedInfoEl)
|
||||
|
||||
const contentShowBtn = collapsedInfoEl.querySelector<HTMLElement>('.atk-show-btn')!
|
||||
const contentShowBtn =
|
||||
collapsedInfoEl.querySelector<HTMLElement>('.atk-show-btn')!
|
||||
contentShowBtn.addEventListener('click', (e) => {
|
||||
e.stopPropagation() // 防止穿透
|
||||
|
||||
|
||||
@@ -6,7 +6,10 @@ import type Render from '../render'
|
||||
*/
|
||||
export default function renderHeader(r: Render) {
|
||||
Object.entries({
|
||||
renderNick, renderVerifyBadge, renderDate, renderUABadge
|
||||
renderNick,
|
||||
renderVerifyBadge,
|
||||
renderDate,
|
||||
renderUABadge,
|
||||
}).forEach(([name, render]) => {
|
||||
render(r)
|
||||
})
|
||||
@@ -16,9 +19,13 @@ function renderNick(r: Render) {
|
||||
r.$headerNick = r.$el.querySelector<HTMLElement>('.atk-nick')!
|
||||
|
||||
if (r.data.link) {
|
||||
const $nickA = Utils.createElement<HTMLLinkElement>('<a target="_blank" rel="noreferrer noopener nofollow"></a>')
|
||||
const $nickA = Utils.createElement<HTMLLinkElement>(
|
||||
'<a target="_blank" rel="noreferrer noopener nofollow"></a>',
|
||||
)
|
||||
$nickA.innerText = r.data.nick
|
||||
$nickA.href = Utils.isValidURL(r.data.link) ? r.data.link : `https://${r.data.link}`
|
||||
$nickA.href = Utils.isValidURL(r.data.link)
|
||||
? r.data.link
|
||||
: `https://${r.data.link}`
|
||||
r.$headerNick.append($nickA)
|
||||
} else {
|
||||
r.$headerNick.innerText = r.data.nick
|
||||
@@ -39,7 +46,9 @@ function renderVerifyBadge(ctx: Render) {
|
||||
}
|
||||
|
||||
if (ctx.data.is_pinned) {
|
||||
const $pinnedBadge = Utils.createElement(`<span class="atk-pinned-badge">${ctx.ctx.$t('pin')}</span>`) // 置顶徽章
|
||||
const $pinnedBadge = Utils.createElement(
|
||||
`<span class="atk-pinned-badge">${ctx.ctx.$t('pin')}</span>`,
|
||||
) // 置顶徽章
|
||||
ctx.$headerBadgeWrap.append($pinnedBadge)
|
||||
}
|
||||
}
|
||||
@@ -62,7 +71,9 @@ function renderUABadge(ctx: Render) {
|
||||
$uaWrap.innerHTML = ''
|
||||
|
||||
if (ctx.data.ip_region) {
|
||||
const $regionBadge = Utils.createElement(`<span class="atk-region-badge"></span>`)
|
||||
const $regionBadge = Utils.createElement(
|
||||
`<span class="atk-region-badge"></span>`,
|
||||
)
|
||||
$regionBadge.innerText = ctx.data.ip_region
|
||||
$uaWrap.append($regionBadge)
|
||||
}
|
||||
@@ -70,7 +81,9 @@ function renderUABadge(ctx: Render) {
|
||||
if (ctx.ctx.conf.uaBadge) {
|
||||
const { browser, os } = ctx.comment.getUserUA()
|
||||
if (String(browser).trim()) {
|
||||
const $uaBrowser = Utils.createElement(`<span class="atk-ua ua-browser"></span>`)
|
||||
const $uaBrowser = Utils.createElement(
|
||||
`<span class="atk-ua ua-browser"></span>`,
|
||||
)
|
||||
$uaBrowser.innerText = browser
|
||||
$uaWrap.append($uaBrowser)
|
||||
}
|
||||
|
||||
@@ -8,12 +8,17 @@ import Pending from './pending'
|
||||
import Actions from './actions'
|
||||
|
||||
const Renders = {
|
||||
Avatar, Header, Content, ReplyAt,
|
||||
ReplyTo, Pending, Actions
|
||||
Avatar,
|
||||
Header,
|
||||
Content,
|
||||
ReplyAt,
|
||||
ReplyTo,
|
||||
Pending,
|
||||
Actions,
|
||||
}
|
||||
|
||||
export default function loadRenders(r: Render) {
|
||||
Object.entries(Renders).forEach(([ name, render ]) => {
|
||||
Object.entries(Renders).forEach(([name, render]) => {
|
||||
render(r)
|
||||
})
|
||||
}
|
||||
@@ -7,6 +7,8 @@ import type Render from '../render'
|
||||
export default function renderPending(r: Render) {
|
||||
if (!r.data.is_pending) return
|
||||
|
||||
const pendingEl = Utils.createElement(`<div class="atk-pending">${r.ctx.$t('pendingMsg')}</div>`)
|
||||
const pendingEl = Utils.createElement(
|
||||
`<div class="atk-pending">${r.ctx.$t('pendingMsg')}</div>`,
|
||||
)
|
||||
r.$body.prepend(pendingEl)
|
||||
}
|
||||
@@ -8,9 +8,14 @@ export default function renderReplyAt(r: Render) {
|
||||
if (r.cConf.isFlatMode || r.data.rid === 0) return // not 平铺模式 或 根评论
|
||||
if (!r.cConf.replyTo) return
|
||||
|
||||
r.$replyAt = Utils.createElement(`<span class="atk-item atk-reply-at"><span class="atk-arrow"></span><span class="atk-nick"></span></span>`)
|
||||
r.$replyAt.querySelector<HTMLElement>('.atk-nick')!.innerText = `${r.cConf.replyTo.nick}`
|
||||
r.$replyAt.onclick = () => { r.comment.getActions().goToReplyComment() }
|
||||
r.$replyAt = Utils.createElement(
|
||||
`<span class="atk-item atk-reply-at"><span class="atk-arrow"></span><span class="atk-nick"></span></span>`,
|
||||
)
|
||||
r.$replyAt.querySelector<HTMLElement>('.atk-nick')!.innerText =
|
||||
`${r.cConf.replyTo.nick}`
|
||||
r.$replyAt.onclick = () => {
|
||||
r.comment.getActions().goToReplyComment()
|
||||
}
|
||||
|
||||
r.$headerBadgeWrap.insertAdjacentElement('afterend', r.$replyAt)
|
||||
}
|
||||
@@ -11,14 +11,20 @@ export default function renderReplyTo(r: Render) {
|
||||
|
||||
r.$replyTo = Utils.createElement(`
|
||||
<div class="atk-reply-to">
|
||||
<div class="atk-meta">${r.ctx.$t('reply')} <span class="atk-nick"></span>:</div>
|
||||
<div class="atk-meta">${r.ctx.$t(
|
||||
'reply',
|
||||
)} <span class="atk-nick"></span>:</div>
|
||||
<div class="atk-content"></div>
|
||||
</div>`)
|
||||
const $nick = r.$replyTo.querySelector<HTMLElement>('.atk-nick')!
|
||||
$nick.innerText = `@${r.cConf.replyTo.nick}`
|
||||
$nick.onclick = () => { r.comment.getActions().goToReplyComment() }
|
||||
$nick.onclick = () => {
|
||||
r.comment.getActions().goToReplyComment()
|
||||
}
|
||||
let replyContent = marked(r.cConf.replyTo.content)
|
||||
if (r.cConf.replyTo.is_collapsed) replyContent = `[${Utils.htmlEncode(r.ctx.$t('collapsed'))}]`
|
||||
r.$replyTo.querySelector<HTMLElement>('.atk-content')!.innerHTML = replyContent
|
||||
if (r.cConf.replyTo.is_collapsed)
|
||||
replyContent = `[${Utils.htmlEncode(r.ctx.$t('collapsed'))}]`
|
||||
r.$replyTo.querySelector<HTMLElement>('.atk-content')!.innerHTML =
|
||||
replyContent
|
||||
r.$body.prepend(r.$replyTo)
|
||||
}
|
||||
@@ -26,16 +26,20 @@ export default class ActionBtn {
|
||||
|
||||
public msgRecTimer?: number // 消息显示复原定时器
|
||||
public msgRecTimerFunc?: Function // 消息显示复原操作
|
||||
public get isMessaging() { return !!this.msgRecTimer } // 消息正在显示
|
||||
public get isMessaging() {
|
||||
return !!this.msgRecTimer
|
||||
} // 消息正在显示
|
||||
|
||||
public isConfirming = false // 正在确认
|
||||
public confirmRecTimer?: number // 确认消息复原定时器
|
||||
|
||||
/** 构造函数 */
|
||||
constructor(opts: ActionBtnOptions|string|(() => string)) {
|
||||
this.$el = Utils.createElement(`<span class="atk-common-action-btn"></span>`)
|
||||
constructor(opts: ActionBtnOptions | string | (() => string)) {
|
||||
this.$el = Utils.createElement(
|
||||
`<span class="atk-common-action-btn"></span>`,
|
||||
)
|
||||
|
||||
this.opts = (typeof opts !== 'object') ? ({ text: opts }) : opts
|
||||
this.opts = typeof opts !== 'object' ? { text: opts } : opts
|
||||
this.$el.innerText = this.getText()
|
||||
|
||||
// 仅管理员可操作
|
||||
@@ -50,7 +54,9 @@ export default class ActionBtn {
|
||||
|
||||
/** 获取按钮文字(动态/静态) */
|
||||
private getText() {
|
||||
return (typeof this.opts.text === 'string') ? this.opts.text : this.opts.text()
|
||||
return typeof this.opts.text === 'string'
|
||||
? this.opts.text
|
||||
: this.opts.text()
|
||||
}
|
||||
|
||||
/** 设置点击事件 */
|
||||
@@ -131,7 +137,12 @@ export default class ActionBtn {
|
||||
}
|
||||
|
||||
/** 设置消息 */
|
||||
public setMsg(text: string, className?: string, duringTime?: number, after?: Function) {
|
||||
public setMsg(
|
||||
text: string,
|
||||
className?: string,
|
||||
duringTime?: number,
|
||||
after?: Function,
|
||||
) {
|
||||
this.setLoading(false)
|
||||
if (className) this.$el.classList.add(className)
|
||||
this.$el.innerText = text
|
||||
|
||||
@@ -9,11 +9,13 @@ const AdminChecker: Checker = {
|
||||
const data = {
|
||||
name: checker.getUser().getData().nick,
|
||||
email: checker.getUser().getData().email,
|
||||
password: inputVal
|
||||
password: inputVal,
|
||||
}
|
||||
|
||||
return (async () => {
|
||||
const resp = await checker.getApi().user.login(data.name, data.email, data.password)
|
||||
const resp = await checker
|
||||
.getApi()
|
||||
.user.login(data.name, data.email, data.password)
|
||||
return resp.token
|
||||
})()
|
||||
},
|
||||
@@ -25,12 +27,12 @@ const AdminChecker: Checker = {
|
||||
onSuccess(checker, userToken, inputVal, formEl) {
|
||||
checker.getUser().update({
|
||||
isAdmin: true,
|
||||
token: userToken
|
||||
token: userToken,
|
||||
})
|
||||
checker.getOpts().onReload()
|
||||
},
|
||||
|
||||
onError(checker, err, inputVal, formEl) {}
|
||||
onError(checker, err, inputVal, formEl) {},
|
||||
}
|
||||
|
||||
export default AdminChecker
|
||||
@@ -7,13 +7,17 @@ import type { CheckerCtx } from '.'
|
||||
export function imgBody(checker: CheckerCtx) {
|
||||
// 图片验证方式
|
||||
const elem = Utils.createElement(
|
||||
`<span><img class="atk-captcha-img" src="${checker.get('img_data') || ''}">${$t('captchaCheck')}</span>`
|
||||
);
|
||||
`<span><img class="atk-captcha-img" src="${
|
||||
checker.get('img_data') || ''
|
||||
}">${$t('captchaCheck')}</span>`,
|
||||
)
|
||||
|
||||
// 刷新验证码
|
||||
elem.querySelector<HTMLElement>('.atk-captcha-img')!.onclick = () => {
|
||||
const imgEl = elem.querySelector('.atk-captcha-img')
|
||||
checker.getApi().captcha.captchaGet()
|
||||
checker
|
||||
.getApi()
|
||||
.captcha.captchaGet()
|
||||
.then((imgData) => {
|
||||
imgEl!.setAttribute('src', imgData)
|
||||
})
|
||||
@@ -26,25 +30,38 @@ export function imgBody(checker: CheckerCtx) {
|
||||
|
||||
/** iframe 形式的通用验证服务 */
|
||||
export function iframeBody(checker: CheckerCtx) {
|
||||
const $iframeWrap = Utils.createElement(`<div class="atk-checker-iframe-wrap"></div>`)
|
||||
const $iframe = Utils.createElement<HTMLIFrameElement>(`<iframe class="atk-fade-in"></iframe>`)
|
||||
const $iframeWrap = Utils.createElement(
|
||||
`<div class="atk-checker-iframe-wrap"></div>`,
|
||||
)
|
||||
const $iframe = Utils.createElement<HTMLIFrameElement>(
|
||||
`<iframe class="atk-fade-in"></iframe>`,
|
||||
)
|
||||
$iframe.style.display = 'none'
|
||||
Ui.showLoading($iframeWrap, { transparentBg: true })
|
||||
$iframe.src = `${checker.getOpts().getIframeURLBase() || ''}/api/captcha/get?t=${+new Date()}`
|
||||
$iframe.src = `${
|
||||
checker.getOpts().getIframeURLBase() || ''
|
||||
}/api/captcha/get?t=${+new Date()}`
|
||||
$iframe.onload = () => {
|
||||
$iframe.style.display = ''
|
||||
Ui.hideLoading($iframeWrap)
|
||||
}
|
||||
$iframeWrap.append($iframe)
|
||||
|
||||
const $closeBtn = Utils.createElement(`<div class="atk-close-btn"><i class="atk-icon atk-icon-close"></i></div>`)
|
||||
const $closeBtn = Utils.createElement(
|
||||
`<div class="atk-close-btn"><i class="atk-icon atk-icon-close"></i></div>`,
|
||||
)
|
||||
$iframeWrap.append($closeBtn)
|
||||
|
||||
checker.hideInteractInput()
|
||||
|
||||
// 轮询状态
|
||||
let stop = false // 打断
|
||||
const sleep = (ms: number) => new Promise((resolve) => { window.setTimeout(() => { resolve(null) }, ms) })
|
||||
const sleep = (ms: number) =>
|
||||
new Promise((resolve) => {
|
||||
window.setTimeout(() => {
|
||||
resolve(null)
|
||||
}, ms)
|
||||
})
|
||||
;(async function queryStatus() {
|
||||
await sleep(1000)
|
||||
if (stop) return
|
||||
@@ -52,7 +69,9 @@ export function iframeBody(checker: CheckerCtx) {
|
||||
try {
|
||||
const resp = await checker.getApi().captcha.captchaStatus()
|
||||
isPass = resp.is_pass
|
||||
} catch { isPass = false }
|
||||
} catch {
|
||||
isPass = false
|
||||
}
|
||||
if (isPass) {
|
||||
checker.triggerSuccess()
|
||||
} else {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
import type { Checker } from '.'
|
||||
import * as render from './captcha-renders'
|
||||
|
||||
@@ -19,8 +18,7 @@ const CaptchaChecker: Checker = {
|
||||
onError(checker, err, inputVal, formEl) {
|
||||
formEl.querySelector<HTMLElement>('.atk-captcha-img')!.click() // 刷新验证码
|
||||
formEl.querySelector<HTMLInputElement>('input[type="text"]')!.value = '' // 清空输入框输入
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export default CaptchaChecker
|
||||
|
||||
@@ -30,7 +30,7 @@ export interface CheckerLauncherOptions {
|
||||
* Checker 发射台
|
||||
*/
|
||||
export default class CheckerLauncher {
|
||||
constructor(private opts: CheckerLauncherOptions) { }
|
||||
constructor(private opts: CheckerLauncherOptions) {}
|
||||
|
||||
public checkCaptcha(payload: CheckerCaptchaPayload) {
|
||||
this.fire(CaptchaChecker, payload, (ctx) => {
|
||||
@@ -43,32 +43,41 @@ export default class CheckerLauncher {
|
||||
this.fire(AdminChecker, payload)
|
||||
}
|
||||
|
||||
public fire(checker: Checker, payload: CheckerPayload, postFire?: (c: CheckerCtx) => void) {
|
||||
public fire(
|
||||
checker: Checker,
|
||||
payload: CheckerPayload,
|
||||
postFire?: (c: CheckerCtx) => void,
|
||||
) {
|
||||
// 显示层
|
||||
const layer = this.opts.getCtx().get('layerManager').create(`checker-${new Date().getTime()}`)
|
||||
const layer = this.opts
|
||||
.getCtx()
|
||||
.get('layerManager')
|
||||
.create(`checker-${new Date().getTime()}`)
|
||||
layer.show()
|
||||
|
||||
// 构建 Checker 的上下文
|
||||
const checkerStore: CheckerStore = {}
|
||||
let hideInteractInput = false
|
||||
const checkerCtx: CheckerCtx = {
|
||||
set: (key, val) => { checkerStore[key] = val },
|
||||
get: (key) => (checkerStore[key]),
|
||||
getOpts: () => (this.opts),
|
||||
getUser: () => (this.opts.getCtx().get('user')),
|
||||
set: (key, val) => {
|
||||
checkerStore[key] = val
|
||||
},
|
||||
get: (key) => checkerStore[key],
|
||||
getOpts: () => this.opts,
|
||||
getUser: () => this.opts.getCtx().get('user'),
|
||||
getApi: () => this.opts.getApi(),
|
||||
hideInteractInput: () => {
|
||||
hideInteractInput = true
|
||||
},
|
||||
triggerSuccess: () => {
|
||||
this.close(checker, layer)
|
||||
if (checker.onSuccess) checker.onSuccess(checkerCtx, "", "", formEl)
|
||||
if (payload.onSuccess) payload.onSuccess("", dialog.$el)
|
||||
if (checker.onSuccess) checker.onSuccess(checkerCtx, '', '', formEl)
|
||||
if (payload.onSuccess) payload.onSuccess('', dialog.$el)
|
||||
},
|
||||
cancel: () => {
|
||||
this.close(checker, layer)
|
||||
if (payload.onCancel) payload.onCancel()
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
if (postFire) postFire(checkerCtx)
|
||||
@@ -79,7 +88,9 @@ export default class CheckerLauncher {
|
||||
|
||||
// 输入框
|
||||
const $input = Utils.createElement<HTMLInputElement>(
|
||||
`<input id="check" type="${checker.inputType || 'text'}" autocomplete="off" required placeholder="">`
|
||||
`<input id="check" type="${
|
||||
checker.inputType || 'text'
|
||||
}" autocomplete="off" required placeholder="">`,
|
||||
)
|
||||
formEl.appendChild($input)
|
||||
setTimeout(() => $input.focus(), 80) // 延迟 Focus
|
||||
@@ -123,14 +134,16 @@ export default class CheckerLauncher {
|
||||
// 请求成功
|
||||
this.close(checker, layer)
|
||||
|
||||
if (checker.onSuccess) checker.onSuccess(checkerCtx, data, inputVal, formEl)
|
||||
if (checker.onSuccess)
|
||||
checker.onSuccess(checkerCtx, data, inputVal, formEl)
|
||||
if (payload.onSuccess) payload.onSuccess(inputVal, dialog.$el)
|
||||
})
|
||||
.catch((err) => {
|
||||
// 请求失败
|
||||
btnTextSet(String(err.msg || String(err)))
|
||||
|
||||
if (checker.onError) checker.onError(checkerCtx, err, inputVal, formEl)
|
||||
if (checker.onError)
|
||||
checker.onError(checkerCtx, err, inputVal, formEl)
|
||||
|
||||
// 错误显示 3s 后恢复按钮
|
||||
const tf = setTimeout(() => btnTextRestore(), 3000)
|
||||
@@ -152,7 +165,9 @@ export default class CheckerLauncher {
|
||||
|
||||
if (hideInteractInput) {
|
||||
$input.style.display = 'none'
|
||||
dialog.$el.querySelector<HTMLElement>('.atk-layer-dialog-actions')!.style.display = 'none'
|
||||
dialog.$el.querySelector<HTMLElement>(
|
||||
'.atk-layer-dialog-actions',
|
||||
)!.style.display = 'none'
|
||||
}
|
||||
|
||||
// 层装载 dialog 元素
|
||||
@@ -173,8 +188,18 @@ export interface Checker {
|
||||
inputType?: 'password' | 'text'
|
||||
body: (checker: CheckerCtx) => HTMLElement
|
||||
request: (checker: CheckerCtx, inputVal: string) => Promise<string>
|
||||
onSuccess?: (checker: CheckerCtx, respData: string, inputVal: string, formEl: HTMLElement) => void
|
||||
onError?: (checker: CheckerCtx, err: any, inputVal: string, formEl: HTMLElement) => void
|
||||
onSuccess?: (
|
||||
checker: CheckerCtx,
|
||||
respData: string,
|
||||
inputVal: string,
|
||||
formEl: HTMLElement,
|
||||
) => void
|
||||
onError?: (
|
||||
checker: CheckerCtx,
|
||||
err: any,
|
||||
inputVal: string,
|
||||
formEl: HTMLElement,
|
||||
) => void
|
||||
}
|
||||
|
||||
interface CheckerStore {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import $t from '@/i18n'
|
||||
import * as Utils from '../lib/utils'
|
||||
|
||||
type BtnClickHandler = (btnEl: HTMLElement, dialog: Dialog) => boolean|void
|
||||
type BtnClickHandler = (btnEl: HTMLElement, dialog: Dialog) => boolean | void
|
||||
|
||||
/**
|
||||
* 对话框
|
||||
@@ -11,18 +11,20 @@ export default class Dialog {
|
||||
public $content: HTMLElement
|
||||
public $actions: HTMLElement
|
||||
|
||||
constructor (contentEl: HTMLElement) {
|
||||
constructor(contentEl: HTMLElement) {
|
||||
this.$el = Utils.createElement(
|
||||
`<div class="atk-layer-dialog-wrap">
|
||||
<div class="atk-layer-dialog">
|
||||
<div class="atk-layer-dialog-content"></div>
|
||||
<div class="atk-layer-dialog-actions"></div>
|
||||
</div>
|
||||
</div>`
|
||||
</div>`,
|
||||
)
|
||||
|
||||
// 按钮
|
||||
this.$actions = this.$el.querySelector<HTMLElement>('.atk-layer-dialog-actions')!
|
||||
this.$actions = this.$el.querySelector<HTMLElement>(
|
||||
'.atk-layer-dialog-actions',
|
||||
)!
|
||||
|
||||
// 内容
|
||||
this.$content = this.$el.querySelector('.atk-layer-dialog-content')!
|
||||
@@ -32,7 +34,7 @@ export default class Dialog {
|
||||
/** 按钮 · 确定 */
|
||||
public setYes(handler: BtnClickHandler) {
|
||||
const btn = Utils.createElement<HTMLButtonElement>(
|
||||
`<button data-action="confirm">${$t('confirm')}</button>`
|
||||
`<button data-action="confirm">${$t('confirm')}</button>`,
|
||||
)
|
||||
btn.onclick = this.onBtnClick(handler)
|
||||
this.$actions.appendChild(btn)
|
||||
@@ -43,7 +45,7 @@ export default class Dialog {
|
||||
/** 按钮 · 取消 */
|
||||
public setNo(handler: BtnClickHandler) {
|
||||
const btn = Utils.createElement<HTMLButtonElement>(
|
||||
`<button data-action="cancel">${$t('cancel')}</button>`
|
||||
`<button data-action="cancel">${$t('cancel')}</button>`,
|
||||
)
|
||||
btn.onclick = this.onBtnClick(handler)
|
||||
this.$actions.appendChild(btn)
|
||||
|
||||
@@ -13,16 +13,22 @@ export interface ErrorDialogOptions {
|
||||
}
|
||||
|
||||
export function showErrorDialog(opts: ErrorDialogOptions) {
|
||||
const errEl = Utils.createElement(`<span>${opts.errMsg},${$t('listLoadFailMsg')}<br/></span>`)
|
||||
const errEl = Utils.createElement(
|
||||
`<span>${opts.errMsg},${$t('listLoadFailMsg')}<br/></span>`,
|
||||
)
|
||||
|
||||
if (opts.retryFn) {
|
||||
const $retryBtn = Utils.createElement(`<span style="cursor:pointer;">${$t('listRetry')}</span>`)
|
||||
const $retryBtn = Utils.createElement(
|
||||
`<span style="cursor:pointer;">${$t('listRetry')}</span>`,
|
||||
)
|
||||
$retryBtn.onclick = () => opts.retryFn && opts.retryFn()
|
||||
errEl.appendChild($retryBtn)
|
||||
}
|
||||
|
||||
if (opts.onOpenSidebar) {
|
||||
const $openSidebar = Utils.createElement('<span atk-only-admin-show> | <span style="cursor:pointer;">打开控制台</span></span>')
|
||||
const $openSidebar = Utils.createElement(
|
||||
'<span atk-only-admin-show> | <span style="cursor:pointer;">打开控制台</span></span>',
|
||||
)
|
||||
errEl.appendChild($openSidebar)
|
||||
$openSidebar.onclick = () => opts.onOpenSidebar && opts.onOpenSidebar()
|
||||
}
|
||||
|
||||
@@ -40,7 +40,8 @@ export default class Pagination {
|
||||
<input type="text" class="atk-input" aria-label="Enter the number of page" />
|
||||
<div class="atk-btn atk-btn-next">Next</div>
|
||||
</div>
|
||||
</div>`)
|
||||
</div>`,
|
||||
)
|
||||
this.$input = this.$el.querySelector('.atk-input')!
|
||||
this.$input.value = `${this.page}`
|
||||
|
||||
@@ -74,11 +75,23 @@ export default class Pagination {
|
||||
const value = this.$input.value.trim()
|
||||
|
||||
const modify = () => {
|
||||
if (value === '') { this.setInput(this.page);return }
|
||||
if (value === '') {
|
||||
this.setInput(this.page)
|
||||
return
|
||||
}
|
||||
let page = Number(value)
|
||||
if (Number.isNaN(page)) { this.setInput(this.page);return }
|
||||
if (page < 1) { this.setInput(this.page);return }
|
||||
if (page > this.maxPage) { this.setInput(this.maxPage);page = this.maxPage }
|
||||
if (Number.isNaN(page)) {
|
||||
this.setInput(this.page)
|
||||
return
|
||||
}
|
||||
if (page < 1) {
|
||||
this.setInput(this.page)
|
||||
return
|
||||
}
|
||||
if (page > this.maxPage) {
|
||||
this.setInput(this.maxPage)
|
||||
page = this.maxPage
|
||||
}
|
||||
this.change(page)
|
||||
}
|
||||
|
||||
@@ -89,13 +102,17 @@ export default class Pagination {
|
||||
|
||||
public prev() {
|
||||
const page = this.page - 1
|
||||
if (page < 1) { return }
|
||||
if (page < 1) {
|
||||
return
|
||||
}
|
||||
this.change(page)
|
||||
}
|
||||
|
||||
public next() {
|
||||
const page = this.page + 1
|
||||
if (page > this.maxPage) { return }
|
||||
if (page > this.maxPage) {
|
||||
return
|
||||
}
|
||||
this.change(page)
|
||||
}
|
||||
|
||||
@@ -130,13 +147,17 @@ export default class Pagination {
|
||||
if (keyCode === 38) {
|
||||
// 上键
|
||||
const page = Number(this.$input.value) + 1
|
||||
if (page > this.maxPage) { return }
|
||||
this.setInput(page);
|
||||
if (page > this.maxPage) {
|
||||
return
|
||||
}
|
||||
this.setInput(page)
|
||||
this.input()
|
||||
} else if (keyCode === 40) {
|
||||
// 下键
|
||||
const page = Number(this.$input.value) - 1
|
||||
if (page < 1) { return }
|
||||
if (page < 1) {
|
||||
return
|
||||
}
|
||||
this.setInput(page)
|
||||
this.input()
|
||||
} else if (keyCode === 13) {
|
||||
@@ -146,7 +167,7 @@ export default class Pagination {
|
||||
}
|
||||
|
||||
/** 加载 */
|
||||
setLoading (isLoading: boolean) {
|
||||
setLoading(isLoading: boolean) {
|
||||
if (isLoading) Ui.showLoading(this.$el)
|
||||
else Ui.hideLoading(this.$el)
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ export default class ReadMoreBtn {
|
||||
|
||||
/** 是否有更多内容 */
|
||||
get hasMore() {
|
||||
return this.total > (this.offset + this.opts.pageSize)
|
||||
return this.total > this.offset + this.opts.pageSize
|
||||
}
|
||||
|
||||
public constructor(opts: ReadMoreBtnOptions) {
|
||||
@@ -31,17 +31,20 @@ export default class ReadMoreBtn {
|
||||
this.origText = this.opts.text || this.origText
|
||||
|
||||
this.$el = Utils.createElement(
|
||||
`<div class="atk-list-read-more" style="display: none;">
|
||||
`<div class="atk-list-read-more" style="display: none;">
|
||||
<div class="atk-list-read-more-inner">
|
||||
<div class="atk-loading-icon" style="display: none;"></div>
|
||||
<span class="atk-text">${this.origText}</span>
|
||||
</div>
|
||||
</div>`)
|
||||
</div>`,
|
||||
)
|
||||
|
||||
this.$loading = this.$el.querySelector<HTMLElement>('.atk-loading-icon')!
|
||||
this.$text = this.$el.querySelector<HTMLElement>('.atk-text')!
|
||||
|
||||
this.$el.onclick = () => { this.click() }
|
||||
this.$el.onclick = () => {
|
||||
this.click()
|
||||
}
|
||||
}
|
||||
|
||||
click() {
|
||||
@@ -60,7 +63,7 @@ export default class ReadMoreBtn {
|
||||
}
|
||||
|
||||
/** 加载 */
|
||||
setLoading (isLoading: boolean) {
|
||||
setLoading(isLoading: boolean) {
|
||||
this.$loading.style.display = isLoading ? '' : 'none'
|
||||
this.$text.style.display = isLoading ? 'none' : ''
|
||||
}
|
||||
|
||||
@@ -9,7 +9,9 @@ import Defaults from './defaults'
|
||||
* @param customConf - The custom config object which is provided by the user
|
||||
* @returns The config for Artalk instance creation
|
||||
*/
|
||||
export function handelCustomConf(customConf: Partial<ArtalkConfig>): ArtalkConfig {
|
||||
export function handelCustomConf(
|
||||
customConf: Partial<ArtalkConfig>,
|
||||
): ArtalkConfig {
|
||||
// 合并默认配置
|
||||
const conf: ArtalkConfig = Utils.mergeDeep(Defaults, customConf)
|
||||
|
||||
@@ -48,12 +50,11 @@ export function handelCustomConf(customConf: Partial<ArtalkConfig>): ArtalkConfi
|
||||
}
|
||||
|
||||
// flatMode
|
||||
if (conf.flatMode === true || Number(conf.nestMax) <= 1)
|
||||
conf.flatMode = true
|
||||
if (conf.flatMode === true || Number(conf.nestMax) <= 1) conf.flatMode = true
|
||||
|
||||
// 自动判断启用平铺模式
|
||||
if (conf.flatMode === 'auto')
|
||||
conf.flatMode = window.matchMedia("(max-width: 768px)").matches
|
||||
conf.flatMode = window.matchMedia('(max-width: 768px)').matches
|
||||
|
||||
return conf
|
||||
}
|
||||
@@ -66,18 +67,23 @@ export function handelCustomConf(customConf: Partial<ArtalkConfig>): ArtalkConfi
|
||||
*/
|
||||
export function handleConfFormServer(conf: Partial<ArtalkConfig>) {
|
||||
const DisabledKeys: (keyof ArtalkConfig)[] = [
|
||||
'el', 'pageKey', 'pageTitle', 'server', 'site', 'darkMode'
|
||||
'el',
|
||||
'pageKey',
|
||||
'pageTitle',
|
||||
'server',
|
||||
'site',
|
||||
'darkMode',
|
||||
]
|
||||
Object.keys(conf).forEach(k => {
|
||||
Object.keys(conf).forEach((k) => {
|
||||
if (DisabledKeys.includes(k as any)) delete conf[k]
|
||||
})
|
||||
|
||||
// Patch: `emoticons` config string to json
|
||||
if (conf.emoticons && typeof conf.emoticons === "string") {
|
||||
if (conf.emoticons && typeof conf.emoticons === 'string') {
|
||||
conf.emoticons = conf.emoticons.trim()
|
||||
if (conf.emoticons.startsWith("[") || conf.emoticons.startsWith("{")) {
|
||||
if (conf.emoticons.startsWith('[') || conf.emoticons.startsWith('{')) {
|
||||
conf.emoticons = JSON.parse(conf.emoticons) // pase json
|
||||
} else if (conf.emoticons === "false") {
|
||||
} else if (conf.emoticons === 'false') {
|
||||
conf.emoticons = false
|
||||
}
|
||||
}
|
||||
@@ -92,7 +98,10 @@ export function handleConfFormServer(conf: Partial<ArtalkConfig>) {
|
||||
* @param ctx - If `ctx` not provided, `checkAdmin` and `checkCaptcha` will be disabled
|
||||
* @returns ApiOptions for Api client instance creation
|
||||
*/
|
||||
export function convertApiOptions(conf: Partial<ArtalkConfig>, ctx?: ContextApi): ApiOptions {
|
||||
export function convertApiOptions(
|
||||
conf: Partial<ArtalkConfig>,
|
||||
ctx?: ContextApi,
|
||||
): ApiOptions {
|
||||
return {
|
||||
baseURL: `${conf.server}/api`,
|
||||
siteName: conf.site || '',
|
||||
@@ -100,15 +109,21 @@ export function convertApiOptions(conf: Partial<ArtalkConfig>, ctx?: ContextApi)
|
||||
pageTitle: conf.pageTitle || '',
|
||||
timeout: conf.reqTimeout,
|
||||
apiToken: ctx?.get('user').getData().token,
|
||||
userInfo: ctx?.get('user').checkHasBasicUserInfo() ? {
|
||||
name: ctx?.get('user').getData().nick,
|
||||
email: ctx?.get('user').getData().email,
|
||||
} : undefined,
|
||||
userInfo: ctx?.get('user').checkHasBasicUserInfo()
|
||||
? {
|
||||
name: ctx?.get('user').getData().nick,
|
||||
email: ctx?.get('user').getData().email,
|
||||
}
|
||||
: undefined,
|
||||
|
||||
onNeedCheckAdmin(payload) {
|
||||
ctx?.checkAdmin({
|
||||
onSuccess: () => { payload.recall() },
|
||||
onCancel: () => { payload.reject() },
|
||||
onSuccess: () => {
|
||||
payload.recall()
|
||||
},
|
||||
onCancel: () => {
|
||||
payload.reject()
|
||||
},
|
||||
})
|
||||
},
|
||||
|
||||
@@ -116,8 +131,12 @@ export function convertApiOptions(conf: Partial<ArtalkConfig>, ctx?: ContextApi)
|
||||
ctx?.checkCaptcha({
|
||||
imgData: payload.data.imgData,
|
||||
iframe: payload.data.iframe,
|
||||
onSuccess: () => { payload.recall() },
|
||||
onCancel: () => { payload.reject() },
|
||||
onSuccess: () => {
|
||||
payload.recall()
|
||||
},
|
||||
onCancel: () => {
|
||||
payload.reject()
|
||||
},
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
import type { ArtalkConfig, CommentData, ListFetchParams, ContextApi, EventPayloadMap, SidebarShowPayload } from '~/types'
|
||||
import type {
|
||||
ArtalkConfig,
|
||||
CommentData,
|
||||
ListFetchParams,
|
||||
ContextApi,
|
||||
EventPayloadMap,
|
||||
SidebarShowPayload,
|
||||
} from '~/types'
|
||||
import type { TInjectedServices } from './service'
|
||||
import Api from './api'
|
||||
|
||||
@@ -13,7 +20,7 @@ import EventManager from './lib/event-manager'
|
||||
import { convertApiOptions, handelCustomConf } from './config'
|
||||
|
||||
// Auto dependency injection
|
||||
interface Context extends TInjectedServices { }
|
||||
interface Context extends TInjectedServices {}
|
||||
|
||||
/**
|
||||
* Artalk Context
|
||||
@@ -122,11 +129,11 @@ class Context implements ContextApi {
|
||||
}
|
||||
|
||||
/* i18n */
|
||||
public $t(key: I18n.I18nKeys, args: {[key: string]: string} = {}): string {
|
||||
public $t(key: I18n.I18nKeys, args: { [key: string]: string } = {}): string {
|
||||
return I18n.t(key, args)
|
||||
}
|
||||
|
||||
public setDarkMode(darkMode: boolean|'auto'): void {
|
||||
public setDarkMode(darkMode: boolean | 'auto'): void {
|
||||
// prevent trigger 'conf-loaded' to improve performance
|
||||
// this.updateConf({ ...this.conf, darkMode })
|
||||
this.conf.darkMode = darkMode
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
import type { NotifyData, PageData, CommentData, DataManagerApi, ListFetchParams, ListLastFetchData, EventPayloadMap } from '~/types'
|
||||
import type {
|
||||
NotifyData,
|
||||
PageData,
|
||||
CommentData,
|
||||
DataManagerApi,
|
||||
ListFetchParams,
|
||||
ListLastFetchData,
|
||||
EventPayloadMap,
|
||||
} from '~/types'
|
||||
import EventManager from './lib/event-manager'
|
||||
|
||||
export class DataManager implements DataManagerApi {
|
||||
@@ -8,9 +16,7 @@ export class DataManager implements DataManagerApi {
|
||||
private unreads: NotifyData[] = []
|
||||
private page?: PageData
|
||||
|
||||
constructor(
|
||||
protected events: EventManager<EventPayloadMap>
|
||||
) {}
|
||||
constructor(protected events: EventManager<EventPayloadMap>) {}
|
||||
|
||||
getLoading() {
|
||||
return this.loading
|
||||
@@ -40,7 +46,7 @@ export class DataManager implements DataManagerApi {
|
||||
}
|
||||
|
||||
findComment(id: number) {
|
||||
return this.comments.find(c => c.id === id)
|
||||
return this.comments.find((c) => c.id === id)
|
||||
}
|
||||
|
||||
clearComments() {
|
||||
@@ -64,7 +70,7 @@ export class DataManager implements DataManagerApi {
|
||||
}
|
||||
|
||||
updateComment(comment: CommentData) {
|
||||
this.comments = this.comments.map(c => {
|
||||
this.comments = this.comments.map((c) => {
|
||||
if (c.id === comment.id) return comment
|
||||
return c
|
||||
})
|
||||
@@ -74,9 +80,9 @@ export class DataManager implements DataManagerApi {
|
||||
}
|
||||
|
||||
deleteComment(id: number) {
|
||||
const comment = this.comments.find(c => c.id === id)
|
||||
const comment = this.comments.find((c) => c.id === id)
|
||||
if (!comment) throw new Error(`Comment ${id} not found`)
|
||||
this.comments = this.comments.filter(c => c.id !== id)
|
||||
this.comments = this.comments.filter((c) => c.id !== id)
|
||||
|
||||
this.events.trigger('comment-deleted', comment)
|
||||
this.events.trigger('list-loaded', this.comments)
|
||||
|
||||
@@ -17,7 +17,7 @@ const defaults: ArtalkConfig = {
|
||||
nestMax: 2,
|
||||
nestSort: 'DATE_ASC',
|
||||
|
||||
emoticons: "https://cdn.jsdelivr.net/gh/ArtalkJS/Emoticons/grps/default.json",
|
||||
emoticons: 'https://cdn.jsdelivr.net/gh/ArtalkJS/Emoticons/grps/default.json',
|
||||
|
||||
vote: true,
|
||||
voteDown: false,
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<div class="atk-main-editor">
|
||||
<div class="atk-header">
|
||||
<input name="nick" class="atk-nick" type="text" required="required">
|
||||
<input name="email" class="atk-email" type="email" required="required">
|
||||
<input name="link" class="atk-link" type="url">
|
||||
<input name="nick" class="atk-nick" type="text" required="required" />
|
||||
<input name="email" class="atk-email" type="email" required="required" />
|
||||
<input name="link" class="atk-link" type="url" />
|
||||
</div>
|
||||
<div class="atk-textarea-wrap">
|
||||
<textarea class="atk-textarea"></textarea>
|
||||
</div>
|
||||
<div class="atk-plug-panel-wrap" style="display: none;"></div>
|
||||
<div class="atk-plug-panel-wrap" style="display: none"></div>
|
||||
<div class="atk-bottom">
|
||||
<div class="atk-item atk-plug-btn-wrap"></div>
|
||||
<div class="atk-item">
|
||||
|
||||
@@ -9,9 +9,15 @@ class Editor extends Component implements EditorApi {
|
||||
private ui: EditorUI
|
||||
private state: EditorStateManager
|
||||
|
||||
getUI() { return this.ui }
|
||||
getPlugs() { return this.ctx.get('editorPlugs') }
|
||||
getState() { return this.state.get() }
|
||||
getUI() {
|
||||
return this.ui
|
||||
}
|
||||
getPlugs() {
|
||||
return this.ctx.get('editorPlugs')
|
||||
}
|
||||
getState() {
|
||||
return this.state.get()
|
||||
}
|
||||
|
||||
constructor(ctx: ContextApi) {
|
||||
super(ctx)
|
||||
@@ -55,14 +61,24 @@ class Editor extends Component implements EditorApi {
|
||||
|
||||
insertContent(val: string) {
|
||||
if ((document as any).selection) {
|
||||
this.ui.$textarea.focus();
|
||||
(document as any).selection.createRange().text = val
|
||||
this.ui.$textarea.focus()
|
||||
} else if (this.ui.$textarea.selectionStart || this.ui.$textarea.selectionStart === 0) {
|
||||
;(document as any).selection.createRange().text = val
|
||||
this.ui.$textarea.focus()
|
||||
} else if (
|
||||
this.ui.$textarea.selectionStart ||
|
||||
this.ui.$textarea.selectionStart === 0
|
||||
) {
|
||||
const sStart = this.ui.$textarea.selectionStart
|
||||
const sEnd = this.ui.$textarea.selectionEnd
|
||||
const sT = this.ui.$textarea.scrollTop
|
||||
this.setContent(this.ui.$textarea.value.substring(0, sStart) + val + this.ui.$textarea.value.substring(sEnd, this.ui.$textarea.value.length))
|
||||
this.setContent(
|
||||
this.ui.$textarea.value.substring(0, sStart) +
|
||||
val +
|
||||
this.ui.$textarea.value.substring(
|
||||
sEnd,
|
||||
this.ui.$textarea.value.length,
|
||||
),
|
||||
)
|
||||
this.ui.$textarea.focus()
|
||||
this.ui.$textarea.selectionStart = sStart + val.length
|
||||
this.ui.$textarea.selectionEnd = sStart + val.length
|
||||
|
||||
@@ -7,7 +7,7 @@ export default class EditorStateManager {
|
||||
constructor(private editor: Editor) {}
|
||||
|
||||
private stateCurt: EditorState = 'normal'
|
||||
private stateUnmountFn: (() => void)|null = null
|
||||
private stateUnmountFn: (() => void) | null = null
|
||||
|
||||
/** Get current state */
|
||||
get() {
|
||||
@@ -20,7 +20,10 @@ export default class EditorStateManager {
|
||||
* @param state The state to switch
|
||||
* @param payload The cause of state switch
|
||||
*/
|
||||
switch(state: EditorState, payload?: { $comment: HTMLElement, comment: CommentData }) {
|
||||
switch(
|
||||
state: EditorState,
|
||||
payload?: { $comment: HTMLElement; comment: CommentData },
|
||||
) {
|
||||
// trigger unmount
|
||||
if (this.stateUnmountFn) {
|
||||
this.stateUnmountFn()
|
||||
@@ -34,13 +37,19 @@ export default class EditorStateManager {
|
||||
if (state !== 'normal' && payload) {
|
||||
// move editor position
|
||||
let moveAfterEl = payload.$comment
|
||||
if (!this.editor.conf.flatMode) moveAfterEl = moveAfterEl.querySelector<HTMLElement>('.atk-footer')!
|
||||
if (!this.editor.conf.flatMode)
|
||||
moveAfterEl = moveAfterEl.querySelector<HTMLElement>('.atk-footer')!
|
||||
this.editor.getPlugs()?.get(Mover)?.move(moveAfterEl)
|
||||
|
||||
const $relative = this.editor.ctx.conf.scrollRelativeTo && this.editor.ctx.conf.scrollRelativeTo()
|
||||
const $relative =
|
||||
this.editor.ctx.conf.scrollRelativeTo &&
|
||||
this.editor.ctx.conf.scrollRelativeTo()
|
||||
Ui.scrollIntoView(this.editor.getUI().$el, true, $relative)
|
||||
|
||||
const plugin = this.editor.getPlugs()?.getPlugs().find(p => p.editorStateEffectWhen === state)
|
||||
const plugin = this.editor
|
||||
.getPlugs()
|
||||
?.getPlugs()
|
||||
.find((p) => p.editorStateEffectWhen === state)
|
||||
if (plugin && plugin.editorStateEffect) {
|
||||
this.stateUnmountFn = plugin.editorStateEffect(payload.comment)
|
||||
}
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
import type { I18n } from '.'
|
||||
|
||||
export const GLOBAL_LOCALES_KEY = "ArtalkI18n"
|
||||
export const GLOBAL_LOCALES_KEY = 'ArtalkI18n'
|
||||
|
||||
export function defineLocaleExternal(lang: string, locale: I18n, aliases?: string[]) {
|
||||
export function defineLocaleExternal(
|
||||
lang: string,
|
||||
locale: I18n,
|
||||
aliases?: string[],
|
||||
) {
|
||||
if (!window[GLOBAL_LOCALES_KEY]) window[GLOBAL_LOCALES_KEY] = {}
|
||||
window[GLOBAL_LOCALES_KEY][lang] = locale
|
||||
if (aliases) aliases.forEach(l => { window[GLOBAL_LOCALES_KEY][l] = locale })
|
||||
if (aliases)
|
||||
aliases.forEach((l) => {
|
||||
window[GLOBAL_LOCALES_KEY][l] = locale
|
||||
})
|
||||
return locale
|
||||
}
|
||||
@@ -9,7 +9,7 @@ export type I18nKeys = keyof I18n
|
||||
// https://en.wikipedia.org/wiki/ISO_639
|
||||
// https://datatracker.ietf.org/doc/html/rfc5646#section-2.1
|
||||
export const internal = {
|
||||
'en': en,
|
||||
en: en,
|
||||
'en-US': en,
|
||||
'zh-CN': zhCN,
|
||||
}
|
||||
@@ -21,7 +21,7 @@ export function findLocaleSet(lang: string): I18n {
|
||||
// normalize a key of language to `ISO 639`
|
||||
lang = lang.replace(
|
||||
/^([a-zA-Z]+)(-[a-zA-Z]+)?$/,
|
||||
(_, p1: string, p2: string) => (p1.toLowerCase() + (p2 || '').toUpperCase())
|
||||
(_, p1: string, p2: string) => p1.toLowerCase() + (p2 || '').toUpperCase(),
|
||||
)
|
||||
|
||||
// internal finding
|
||||
@@ -42,22 +42,22 @@ export function findLocaleSet(lang: string): I18n {
|
||||
/**
|
||||
* System locale setting
|
||||
*/
|
||||
let LocaleConf: I18n|string = 'en'
|
||||
let LocaleConf: I18n | string = 'en'
|
||||
let LocaleDict: I18n = findLocaleSet(LocaleConf) // en by default
|
||||
|
||||
/**
|
||||
* Set system locale
|
||||
*/
|
||||
export function setLocale(locale: I18n|string) {
|
||||
export function setLocale(locale: I18n | string) {
|
||||
if (locale === LocaleConf) return
|
||||
LocaleConf = locale
|
||||
LocaleDict = (typeof locale === 'string') ? findLocaleSet(locale) : locale
|
||||
LocaleDict = typeof locale === 'string' ? findLocaleSet(locale) : locale
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an i18n message by key
|
||||
*/
|
||||
export function t(key: I18nKeys, args: {[key: string]: string} = {}) {
|
||||
export function t(key: I18nKeys, args: { [key: string]: string } = {}) {
|
||||
let str = LocaleDict?.[key] || key
|
||||
str = str.replace(/\{\s*(\w+?)\s*\}/g, (_, token) => args[token] || '')
|
||||
|
||||
|
||||
@@ -1,86 +1,92 @@
|
||||
import { defineLocaleExternal } from './external'
|
||||
|
||||
export default defineLocaleExternal('jp', {
|
||||
/* エディタ */
|
||||
placeholder: 'コメントを残す',
|
||||
noComment: 'コメントなし',
|
||||
send: '送信',
|
||||
save: '保存',
|
||||
nick: 'ニックネーム',
|
||||
email: 'メール',
|
||||
link: 'ウェブサイト',
|
||||
emoticon: '絵文字',
|
||||
preview: 'プレビュー',
|
||||
image: '画像',
|
||||
uploadFail: 'アップロードに失敗しました',
|
||||
commentFail: 'コメントに失敗しました',
|
||||
restoredMsg: 'コンテンツが復元されました',
|
||||
onlyAdminCanReply: '管理者のみが返信できます',
|
||||
uploadLoginMsg: 'アップロードするには、名前とメールアドレスを入力してください',
|
||||
export default defineLocaleExternal(
|
||||
'jp',
|
||||
{
|
||||
/* エディタ */
|
||||
placeholder: 'コメントを残す',
|
||||
noComment: 'コメントなし',
|
||||
send: '送信',
|
||||
save: '保存',
|
||||
nick: 'ニックネーム',
|
||||
email: 'メール',
|
||||
link: 'ウェブサイト',
|
||||
emoticon: '絵文字',
|
||||
preview: 'プレビュー',
|
||||
image: '画像',
|
||||
uploadFail: 'アップロードに失敗しました',
|
||||
commentFail: 'コメントに失敗しました',
|
||||
restoredMsg: 'コンテンツが復元されました',
|
||||
onlyAdminCanReply: '管理者のみが返信できます',
|
||||
uploadLoginMsg:
|
||||
'アップロードするには、名前とメールアドレスを入力してください',
|
||||
|
||||
/* リスト */
|
||||
counter: '{count} コメント',
|
||||
sortLatest: '最新のもの',
|
||||
sortOldest: '最も古い',
|
||||
sortBest: 'ベスト',
|
||||
sortAuthor: '著者',
|
||||
openComment: 'コメントを開く',
|
||||
closeComment: 'コメントを閉じる',
|
||||
listLoadFailMsg: 'コメントの読み込みに失敗しました',
|
||||
listRetry: 'クリックで再試行',
|
||||
loadMore: 'もっと読み込む',
|
||||
/* リスト */
|
||||
counter: '{count} コメント',
|
||||
sortLatest: '最新のもの',
|
||||
sortOldest: '最も古い',
|
||||
sortBest: 'ベスト',
|
||||
sortAuthor: '著者',
|
||||
openComment: 'コメントを開く',
|
||||
closeComment: 'コメントを閉じる',
|
||||
listLoadFailMsg: 'コメントの読み込みに失敗しました',
|
||||
listRetry: 'クリックで再試行',
|
||||
loadMore: 'もっと読み込む',
|
||||
|
||||
/* コメント */
|
||||
admin: '管理者',
|
||||
reply: '返信',
|
||||
voteUp: 'アップ',
|
||||
voteDown: 'ダウン',
|
||||
voteFail: '投票に失敗しました',
|
||||
readMore: '続きを読む',
|
||||
actionConfirm: '確認',
|
||||
collapse: '折りたたむ',
|
||||
collapsed: '折りたたまれた',
|
||||
collapsedMsg: 'このコメントは折りたたまれました',
|
||||
expand: '展開',
|
||||
approved: '承認されました',
|
||||
pending: '保留中',
|
||||
pendingMsg: '保留中、コメント投稿者にのみ表示されます',
|
||||
edit: '編集',
|
||||
editCancel: '編集をキャンセル',
|
||||
delete: '削除',
|
||||
deleteConfirm: '確認',
|
||||
pin: 'ピン',
|
||||
unpin: 'ピン外し',
|
||||
/* コメント */
|
||||
admin: '管理者',
|
||||
reply: '返信',
|
||||
voteUp: 'アップ',
|
||||
voteDown: 'ダウン',
|
||||
voteFail: '投票に失敗しました',
|
||||
readMore: '続きを読む',
|
||||
actionConfirm: '確認',
|
||||
collapse: '折りたたむ',
|
||||
collapsed: '折りたたまれた',
|
||||
collapsedMsg: 'このコメントは折りたたまれました',
|
||||
expand: '展開',
|
||||
approved: '承認されました',
|
||||
pending: '保留中',
|
||||
pendingMsg: '保留中、コメント投稿者にのみ表示されます',
|
||||
edit: '編集',
|
||||
editCancel: '編集をキャンセル',
|
||||
delete: '削除',
|
||||
deleteConfirm: '確認',
|
||||
pin: 'ピン',
|
||||
unpin: 'ピン外し',
|
||||
|
||||
/* 時間 */
|
||||
seconds: '秒前',
|
||||
minutes: '分前',
|
||||
hours: '時間前',
|
||||
days: '数日前',
|
||||
now: 'たった今',
|
||||
/* 時間 */
|
||||
seconds: '秒前',
|
||||
minutes: '分前',
|
||||
hours: '時間前',
|
||||
days: '数日前',
|
||||
now: 'たった今',
|
||||
|
||||
/* チェッカー */
|
||||
adminCheck: '管理者パスワードを入力してください:',
|
||||
captchaCheck: '続けるにはCAPTCHAを入力してください:',
|
||||
confirm: '確認',
|
||||
cancel: '取消',
|
||||
/* チェッカー */
|
||||
adminCheck: '管理者パスワードを入力してください:',
|
||||
captchaCheck: '続けるにはCAPTCHAを入力してください:',
|
||||
confirm: '確認',
|
||||
cancel: '取消',
|
||||
|
||||
/* サイドバー */
|
||||
msgCenter: 'メッセージ',
|
||||
ctrlCenter: 'コンソール',
|
||||
/* サイドバー */
|
||||
msgCenter: 'メッセージ',
|
||||
ctrlCenter: 'コンソール',
|
||||
|
||||
/* 一般 */
|
||||
frontend: 'フロントエンド',
|
||||
backend: 'バックエンド',
|
||||
loading: '読み込み中',
|
||||
loadFail: '読み込みに失敗しました',
|
||||
editing: '編集中',
|
||||
editFail: '編集に失敗しました',
|
||||
deleting: '削除中',
|
||||
deleteFail: '削除に失敗しました',
|
||||
reqGot: 'リクエストを取得しました',
|
||||
reqAborted: 'リクエストがタイムアウトしたか、予期せず終了した',
|
||||
updateMsg: 'フルな体験をするためには、Artalk {name} をアップデートしてください',
|
||||
currentVersion: '現在のバージョン',
|
||||
ignore: '無視する',
|
||||
}, ['jp-JP'])
|
||||
/* 一般 */
|
||||
frontend: 'フロントエンド',
|
||||
backend: 'バックエンド',
|
||||
loading: '読み込み中',
|
||||
loadFail: '読み込みに失敗しました',
|
||||
editing: '編集中',
|
||||
editFail: '編集に失敗しました',
|
||||
deleting: '削除中',
|
||||
deleteFail: '削除に失敗しました',
|
||||
reqGot: 'リクエストを取得しました',
|
||||
reqAborted: 'リクエストがタイムアウトしたか、予期せず終了した',
|
||||
updateMsg:
|
||||
'フルな体験をするためには、Artalk {name} をアップデートしてください',
|
||||
currentVersion: '現在のバージョン',
|
||||
ignore: '無視する',
|
||||
},
|
||||
['jp-JP'],
|
||||
)
|
||||
@@ -17,8 +17,15 @@ export function getScrollbarHelper() {
|
||||
|
||||
lock() {
|
||||
document.body.style.overflow = 'hidden'
|
||||
const barPaddingRight = parseInt(window.getComputedStyle(document.body, null).getPropertyValue('padding-right'), 10)
|
||||
document.body.style.paddingRight = `${Ui.getScrollBarWidth() + barPaddingRight || 0}px`
|
||||
}
|
||||
const barPaddingRight = parseInt(
|
||||
window
|
||||
.getComputedStyle(document.body, null)
|
||||
.getPropertyValue('padding-right'),
|
||||
10,
|
||||
)
|
||||
document.body.style.paddingRight = `${
|
||||
Ui.getScrollBarWidth() + barPaddingRight || 0
|
||||
}px`
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
<div class="atk-sidebar-layer">
|
||||
<div class="atk-sidebar-inner">
|
||||
<div class="atk-sidebar-header">
|
||||
<div class="atk-sidebar-close"><i class="atk-icon atk-icon-close"></i></div>
|
||||
<div class="atk-sidebar-close">
|
||||
<i class="atk-icon atk-icon-close"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="atk-sidebar-iframe-wrap"></div>
|
||||
</div>
|
||||
|
||||
@@ -25,7 +25,9 @@ export default class SidebarLayer extends Component {
|
||||
}
|
||||
|
||||
// event
|
||||
this.ctx.on('user-changed', () => { this.firstShow = true })
|
||||
this.ctx.on('user-changed', () => {
|
||||
this.firstShow = true
|
||||
})
|
||||
}
|
||||
|
||||
private firstShow = true
|
||||
@@ -40,7 +42,7 @@ export default class SidebarLayer extends Component {
|
||||
|
||||
// 管理员身份验证 (若身份失效,弹出验证窗口)
|
||||
this.authCheck({
|
||||
onSuccess: () => this.show(conf) // retry show after auth check
|
||||
onSuccess: () => this.show(conf), // retry show after auth check
|
||||
})
|
||||
|
||||
// 第一次加载
|
||||
@@ -100,7 +102,7 @@ export default class SidebarLayer extends Component {
|
||||
},
|
||||
onCancel: () => {
|
||||
this.hide()
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -119,24 +121,25 @@ export default class SidebarLayer extends Component {
|
||||
const $iframe = Utils.createElement<HTMLIFrameElement>('<iframe></iframe>')
|
||||
|
||||
// 准备 Iframe 参数
|
||||
const baseURL = (import.meta.env.DEV) ? 'http://localhost:23367/'
|
||||
const baseURL = import.meta.env.DEV
|
||||
? 'http://localhost:23367/'
|
||||
: Utils.getURLBasedOnApi({
|
||||
base: this.ctx.conf.server,
|
||||
path: '/sidebar/',
|
||||
})
|
||||
base: this.ctx.conf.server,
|
||||
path: '/sidebar/',
|
||||
})
|
||||
|
||||
const query: any = {
|
||||
pageKey: this.conf.pageKey,
|
||||
site: this.conf.site || '',
|
||||
user: JSON.stringify(this.ctx.get('user').getData()),
|
||||
time: +new Date()
|
||||
time: +new Date(),
|
||||
}
|
||||
|
||||
if (view) query.view = view
|
||||
if (this.conf.darkMode) query.darkMode = '1'
|
||||
if (typeof this.conf.locale === 'string') query.locale = this.conf.locale
|
||||
|
||||
const urlParams = new URLSearchParams(query);
|
||||
const urlParams = new URLSearchParams(query)
|
||||
this.iframeLoad($iframe, `${baseURL}?${urlParams.toString()}`)
|
||||
|
||||
return $iframe
|
||||
|
||||
@@ -9,7 +9,7 @@ export class LayerWrap {
|
||||
|
||||
constructor() {
|
||||
this.$wrap = Utils.createElement(
|
||||
`<div class="atk-layer-wrap" style="display: none;"><div class="atk-layer-mask"></div></div>`
|
||||
`<div class="atk-layer-wrap" style="display: none;"><div class="atk-layer-mask"></div></div>`,
|
||||
)
|
||||
this.$mask = this.$wrap.querySelector<HTMLElement>('.atk-layer-mask')!
|
||||
}
|
||||
@@ -50,7 +50,10 @@ export class LayerWrap {
|
||||
|
||||
hide(callback?: () => void) {
|
||||
// if wrap contains more than one item, do not hide entire wrap
|
||||
if (this.items.filter(e => e.isConnected && e.style.display !== 'none').length > 1) {
|
||||
if (
|
||||
this.items.filter((e) => e.isConnected && e.style.display !== 'none')
|
||||
.length > 1
|
||||
) {
|
||||
callback && callback()
|
||||
return
|
||||
}
|
||||
|
||||
@@ -4,9 +4,7 @@ export default abstract class Component {
|
||||
public $el!: HTMLElement
|
||||
public readonly conf: ArtalkConfig
|
||||
|
||||
public constructor(
|
||||
public ctx: ContextApi
|
||||
) {
|
||||
public constructor(public ctx: ContextApi) {
|
||||
this.conf = ctx.conf
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@ function Detect(userAgent?: string) {
|
||||
browser: '',
|
||||
device: '',
|
||||
language: '',
|
||||
version: ''
|
||||
version: '',
|
||||
}
|
||||
|
||||
// 内核
|
||||
@@ -18,7 +18,7 @@ function Detect(userAgent?: string) {
|
||||
Trident: u.includes('Trident') || u.includes('NET CLR'),
|
||||
Presto: u.includes('Presto'),
|
||||
WebKit: u.includes('AppleWebKit'),
|
||||
Gecko: u.includes('Gecko/')
|
||||
Gecko: u.includes('Gecko/'),
|
||||
}
|
||||
|
||||
// 浏览器
|
||||
@@ -60,7 +60,7 @@ function Detect(userAgent?: string) {
|
||||
const osMatch = {
|
||||
Windows: u.includes('Windows'),
|
||||
Linux: u.includes('Linux') || u.includes('X11'),
|
||||
'macOS': u.includes('Macintosh'),
|
||||
macOS: u.includes('Macintosh'),
|
||||
Android: u.includes('Android') || u.includes('Adr'),
|
||||
Ubuntu: u.includes('Ubuntu'),
|
||||
FreeBSD: u.includes('FreeBSD'),
|
||||
@@ -77,12 +77,12 @@ function Detect(userAgent?: string) {
|
||||
// 设备
|
||||
const deviceMatch = {
|
||||
Mobile: u.includes('Mobi') || u.includes('iPh') || u.includes('480'),
|
||||
Tablet: u.includes('Tablet') || u.includes('Pad') || u.includes('Nexus 7')
|
||||
Tablet: u.includes('Tablet') || u.includes('Pad') || u.includes('Nexus 7'),
|
||||
}
|
||||
|
||||
// 修正
|
||||
if (deviceMatch.Mobile) {
|
||||
deviceMatch.Mobile = !(u.includes('iPad'))
|
||||
deviceMatch.Mobile = !u.includes('iPad')
|
||||
} else if (browserMatch.Chrome && u.includes('Edg')) {
|
||||
// Chrome 内核的 Edge
|
||||
browserMatch.Chrome = false
|
||||
@@ -97,7 +97,7 @@ function Detect(userAgent?: string) {
|
||||
|
||||
// 语言
|
||||
dest.language = (() => {
|
||||
const g = ((nav as any).browserLanguage || nav.language)
|
||||
const g = (nav as any).browserLanguage || nav.language
|
||||
const arr = g.split('-')
|
||||
if (arr[1]) arr[1] = arr[1].toUpperCase()
|
||||
return arr.join('_')
|
||||
@@ -130,16 +130,17 @@ function Detect(userAgent?: string) {
|
||||
'5.1': 'XP',
|
||||
'5.0': '2000',
|
||||
'10.0': '10',
|
||||
'11.0': '11' // 自定的,不是微软官方的判断方法
|
||||
'11.0': '11', // 自定的,不是微软官方的判断方法
|
||||
}
|
||||
return wvHash[v] || v
|
||||
},
|
||||
Android: () => u.replace(/^.*Android ([\d.]+);.*$/, '$1'),
|
||||
iOS: () => u.replace(/^.*OS ([\d_]+) like.*$/, '$1').replace(/_/g, '.'),
|
||||
Debian: () => u.replace(/^.*Debian\/([\d.]+).*$/, '$1'),
|
||||
'Windows Phone': () => u.replace(/^.*Windows Phone( OS)? ([\d.]+);.*$/, '$2'),
|
||||
'macOS': () => u.replace(/^.*Mac OS X ([\d_]+).*$/, '$1').replace(/_/g, '.'),
|
||||
WebOS: () => u.replace(/^.*hpwOS\/([\d.]+);.*$/, '$1')
|
||||
'Windows Phone': () =>
|
||||
u.replace(/^.*Windows Phone( OS)? ([\d.]+);.*$/, '$2'),
|
||||
macOS: () => u.replace(/^.*Mac OS X ([\d_]+).*$/, '$1').replace(/_/g, '.'),
|
||||
WebOS: () => u.replace(/^.*hpwOS\/([\d.]+);.*$/, '$1'),
|
||||
}
|
||||
|
||||
dest.osVersion = ''
|
||||
@@ -153,13 +154,23 @@ function Detect(userAgent?: string) {
|
||||
// 浏览器版本信息
|
||||
const version = {
|
||||
Safari: () => u.replace(/^.*Version\/([\d.]+).*$/, '$1'),
|
||||
Chrome: () => u.replace(/^.*Chrome\/([\d.]+).*$/, '$1').replace(/^.*CriOS\/([\d.]+).*$/, '$1'),
|
||||
IE: () => u.replace(/^.*MSIE ([\d.]+).*$/, '$1').replace(/^.*rv:([\d.]+).*$/, '$1'),
|
||||
Chrome: () =>
|
||||
u
|
||||
.replace(/^.*Chrome\/([\d.]+).*$/, '$1')
|
||||
.replace(/^.*CriOS\/([\d.]+).*$/, '$1'),
|
||||
IE: () =>
|
||||
u.replace(/^.*MSIE ([\d.]+).*$/, '$1').replace(/^.*rv:([\d.]+).*$/, '$1'),
|
||||
Edge: () => u.replace(/^.*(Edge|Edg|Edg[A-Z]{1})\/([\d.]+).*$/, '$2'),
|
||||
Firefox: () => u.replace(/^.*Firefox\/([\d.]+).*$/, '$1').replace(/^.*FxiOS\/([\d.]+).*$/, '$1'),
|
||||
Firefox: () =>
|
||||
u
|
||||
.replace(/^.*Firefox\/([\d.]+).*$/, '$1')
|
||||
.replace(/^.*FxiOS\/([\d.]+).*$/, '$1'),
|
||||
'Firefox Focus': () => u.replace(/^.*Focus\/([\d.]+).*$/, '$1'),
|
||||
Chromium: () => u.replace(/^.*Chromium\/([\d.]+).*$/, '$1'),
|
||||
Opera: () => u.replace(/^.*Opera\/([\d.]+).*$/, '$1').replace(/^.*OPR\/([\d.]+).*$/, '$1'),
|
||||
Opera: () =>
|
||||
u
|
||||
.replace(/^.*Opera\/([\d.]+).*$/, '$1')
|
||||
.replace(/^.*OPR\/([\d.]+).*$/, '$1'),
|
||||
Vivaldi: () => u.replace(/^.*Vivaldi\/([\d.]+).*$/, '$1'),
|
||||
Yandex: () => u.replace(/^.*YaBrowser\/([\d.]+).*$/, '$1'),
|
||||
Kindle: () => u.replace(/^.*Version\/([\d.]+).*$/, '$1'),
|
||||
@@ -168,7 +179,10 @@ function Detect(userAgent?: string) {
|
||||
QQ: () => u.replace(/^.*QQ\/([\d.]+).*$/, '$1'),
|
||||
Baidu: () => u.replace(/^.*BIDUBrowser[\s/]([\d.]+).*$/, '$1'),
|
||||
UC: () => u.replace(/^.*UC?Browser\/([\d.]+).*$/, '$1'),
|
||||
Sogou: () => u.replace(/^.*SE ([\d.X]+).*$/, '$1').replace(/^.*SogouMobileBrowser\/([\d.]+).*$/, '$1'),
|
||||
Sogou: () =>
|
||||
u
|
||||
.replace(/^.*SE ([\d.X]+).*$/, '$1')
|
||||
.replace(/^.*SogouMobileBrowser\/([\d.]+).*$/, '$1'),
|
||||
'2345Explorer': () => u.replace(/^.*2345Explorer\/([\d.]+).*$/, '$1'),
|
||||
TheWorld: () => u.replace(/^.*TheWorld ([\d.]+).*$/, '$1'),
|
||||
MIUI: () => u.replace(/^.*MiuiBrowser\/([\d.]+).*$/, '$1'),
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
export type EventHandler<T> = (payload: T) => void
|
||||
export interface Event<PayloadMap, K extends keyof PayloadMap = keyof PayloadMap> extends EventOptions {
|
||||
export interface Event<
|
||||
PayloadMap,
|
||||
K extends keyof PayloadMap = keyof PayloadMap,
|
||||
> extends EventOptions {
|
||||
name: K
|
||||
handler: EventHandler<PayloadMap[K]>
|
||||
}
|
||||
@@ -8,28 +11,49 @@ export interface EventOptions {
|
||||
}
|
||||
|
||||
export interface EventManagerFuncs<PayloadMap> {
|
||||
on<K extends keyof PayloadMap>(name: K, handler: EventHandler<PayloadMap[K]>, opts?: EventOptions): void
|
||||
off<K extends keyof PayloadMap>(name: K, handler: EventHandler<PayloadMap[K]>): void
|
||||
on<K extends keyof PayloadMap>(
|
||||
name: K,
|
||||
handler: EventHandler<PayloadMap[K]>,
|
||||
opts?: EventOptions,
|
||||
): void
|
||||
off<K extends keyof PayloadMap>(
|
||||
name: K,
|
||||
handler: EventHandler<PayloadMap[K]>,
|
||||
): void
|
||||
trigger<K extends keyof PayloadMap>(name: K, payload?: PayloadMap[K]): void
|
||||
}
|
||||
|
||||
export default class EventManager<PayloadMap> implements EventManagerFuncs<PayloadMap> {
|
||||
export default class EventManager<PayloadMap>
|
||||
implements EventManagerFuncs<PayloadMap>
|
||||
{
|
||||
private events: Event<PayloadMap>[] = []
|
||||
|
||||
/**
|
||||
* Add an event listener for a specific event name
|
||||
*/
|
||||
public on<K extends keyof PayloadMap>(name: K, handler: EventHandler<PayloadMap[K]>, opts: EventOptions = {}) {
|
||||
this.events.push({ name, handler: handler as EventHandler<PayloadMap[keyof PayloadMap]>, ...opts })
|
||||
public on<K extends keyof PayloadMap>(
|
||||
name: K,
|
||||
handler: EventHandler<PayloadMap[K]>,
|
||||
opts: EventOptions = {},
|
||||
) {
|
||||
this.events.push({
|
||||
name,
|
||||
handler: handler as EventHandler<PayloadMap[keyof PayloadMap]>,
|
||||
...opts,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an event listener for a specific event name and handler
|
||||
*/
|
||||
public off<K extends keyof PayloadMap>(name: K, handler: EventHandler<PayloadMap[K]>) {
|
||||
public off<K extends keyof PayloadMap>(
|
||||
name: K,
|
||||
handler: EventHandler<PayloadMap[K]>,
|
||||
) {
|
||||
if (!handler) return // not allow remove all events with same name
|
||||
this.events = this.events.filter((evt) =>
|
||||
!(evt.name === name && evt.handler === handler))
|
||||
this.events = this.events.filter(
|
||||
(evt) => !(evt.name === name && evt.handler === handler),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -8,25 +8,41 @@ export function getRenderer() {
|
||||
return renderer
|
||||
}
|
||||
|
||||
export const markedLinkRenderer = (renderer: any, orgLinkRenderer: Function) => (href: string, title: string, text: string): string => {
|
||||
const localLink = href?.startsWith(`${window.location.protocol}//${window.location.hostname}`);
|
||||
const html = orgLinkRenderer.call(renderer, href, title, text);
|
||||
return html.replace(/^<a /, `<a target="_blank" ${!localLink ? `rel="noreferrer noopener nofollow"` : ''} `);
|
||||
}
|
||||
|
||||
export const markedCodeRenderer = () => (block: string, lang: string | undefined): string => {
|
||||
// Colorize the block only if the language is known to highlight.js
|
||||
const realLang = (!lang ? 'plaintext' : lang)
|
||||
let colorized = block
|
||||
if ((window as any).hljs) {
|
||||
if (realLang && (window as any).hljs.getLanguage(realLang)) {
|
||||
colorized = (window as any).hljs.highlight(realLang, block).value
|
||||
}
|
||||
} else {
|
||||
colorized = renderCode(block)
|
||||
export const markedLinkRenderer =
|
||||
(renderer: any, orgLinkRenderer: Function) =>
|
||||
(href: string, title: string, text: string): string => {
|
||||
const localLink = href?.startsWith(
|
||||
`${window.location.protocol}//${window.location.hostname}`,
|
||||
)
|
||||
const html = orgLinkRenderer.call(renderer, href, title, text)
|
||||
return html.replace(
|
||||
/^<a /,
|
||||
`<a target="_blank" ${
|
||||
!localLink ? `rel="noreferrer noopener nofollow"` : ''
|
||||
} `,
|
||||
)
|
||||
}
|
||||
|
||||
return `<pre rel="${realLang}">\n`
|
||||
+ `<code class="hljs language-${realLang}">${colorized.replace(/&/g, '&')}</code>\n`
|
||||
+ `</pre>`
|
||||
}
|
||||
export const markedCodeRenderer =
|
||||
() =>
|
||||
(block: string, lang: string | undefined): string => {
|
||||
// Colorize the block only if the language is known to highlight.js
|
||||
const realLang = !lang ? 'plaintext' : lang
|
||||
let colorized = block
|
||||
if ((window as any).hljs) {
|
||||
if (realLang && (window as any).hljs.getLanguage(realLang)) {
|
||||
colorized = (window as any).hljs.highlight(realLang, block).value
|
||||
}
|
||||
} else {
|
||||
colorized = renderCode(block)
|
||||
}
|
||||
|
||||
return (
|
||||
`<pre rel="${realLang}">\n` +
|
||||
`<code class="hljs language-${realLang}">${colorized.replace(
|
||||
/&/g,
|
||||
'&',
|
||||
)}</code>\n` +
|
||||
`</pre>`
|
||||
)
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import { getRenderer } from './marked-renderer'
|
||||
type Replacer = (raw: string) => string
|
||||
export type TMarked = typeof libMarked
|
||||
|
||||
let instance: (typeof libMarked)|undefined
|
||||
let instance: typeof libMarked | undefined
|
||||
let replacers: Replacer[] = []
|
||||
|
||||
const markedOptions: MarkedOptions = {
|
||||
@@ -32,12 +32,16 @@ export function setReplacers(arr: Replacer[]) {
|
||||
|
||||
/** 初始化 marked */
|
||||
export function initMarked() {
|
||||
try { if (!libMarked.name) return } catch { return }
|
||||
try {
|
||||
if (!libMarked.name) return
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
// @see https://github.com/markedjs/marked/blob/4afb228d956a415624c4e5554bb8f25d047676fe/src/Tokenizer.js#L329
|
||||
libMarked.setOptions({
|
||||
renderer: getRenderer(),
|
||||
...markedOptions
|
||||
...markedOptions,
|
||||
})
|
||||
|
||||
instance = libMarked
|
||||
@@ -46,7 +50,8 @@ export function initMarked() {
|
||||
/** 解析 markdown */
|
||||
export default function marked(src: string): string {
|
||||
let markedContent = getInstance()?.parse(src)
|
||||
if (!markedContent) { // 无 Markdown 模式简单处理
|
||||
if (!markedContent) {
|
||||
// 无 Markdown 模式简单处理
|
||||
markedContent = simpleMarked(src)
|
||||
}
|
||||
|
||||
@@ -54,19 +59,29 @@ export default function marked(src: string): string {
|
||||
|
||||
// 内容替换器
|
||||
replacers.forEach((replacer) => {
|
||||
if (typeof replacer === 'function')
|
||||
dest = replacer(dest)
|
||||
if (typeof replacer === 'function') dest = replacer(dest)
|
||||
})
|
||||
|
||||
return dest
|
||||
}
|
||||
|
||||
function simpleMarked(src: string) {
|
||||
return src
|
||||
// .replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
|
||||
.replace(/```\s*([^]+?.*?[^]+?[^]+?)```/g, (_, code) => `<pre><code>${renderCode(code)}</code></pre>`)
|
||||
// .replace(/`([^`]+?)`/g, '<code>$1</code>')
|
||||
.replace(/!\[(.*?)\]\((.*?)\)/g, (_, alt, imgSrc) => `<img src="${imgSrc}" alt="${alt}" />`)
|
||||
.replace(/\[(.*?)\]\((.*?)\)/g, (_, text, link) => `<a href="${link}" target="_blank">${text}</a>`)
|
||||
.replace(/\n/g, '<br>')
|
||||
return (
|
||||
src
|
||||
// .replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
|
||||
.replace(
|
||||
/```\s*([^]+?.*?[^]+?[^]+?)```/g,
|
||||
(_, code) => `<pre><code>${renderCode(code)}</code></pre>`,
|
||||
)
|
||||
// .replace(/`([^`]+?)`/g, '<code>$1</code>')
|
||||
.replace(
|
||||
/!\[(.*?)\]\((.*?)\)/g,
|
||||
(_, alt, imgSrc) => `<img src="${imgSrc}" alt="${alt}" />`,
|
||||
)
|
||||
.replace(
|
||||
/\[(.*?)\]\((.*?)\)/g,
|
||||
(_, text, link) => `<a href="${link}" target="_blank">${text}</a>`,
|
||||
)
|
||||
.replace(/\n/g, '<br>')
|
||||
)
|
||||
}
|
||||
@@ -7,14 +7,56 @@ const insaneOptions = {
|
||||
// @link https://leucosite.com/Microsoft-Edge-RCE/
|
||||
// @link https://medium.com/@knownsec404team/analysis-of-the-security-issues-of-url-scheme-in-pc-from-cve-2018-8495-934478a36756
|
||||
allowedSchemes: [
|
||||
'http', 'https', 'mailto',
|
||||
'data' // for support base64 encoded image (安全性有待考虑)
|
||||
'http',
|
||||
'https',
|
||||
'mailto',
|
||||
'data', // for support base64 encoded image (安全性有待考虑)
|
||||
],
|
||||
allowedTags: [
|
||||
'a', 'abbr', 'article', 'b', 'blockquote', 'br', 'caption', 'code', 'del', 'details', 'div', 'em',
|
||||
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'i', 'img', 'ins', 'kbd', 'li', 'main', 'mark',
|
||||
'ol', 'p', 'pre', 'section', 'span', 'strike', 'strong', 'sub', 'summary', 'sup', 'table',
|
||||
'tbody', 'td', 'th', 'thead', 'tr', 'u', 'ul'
|
||||
'a',
|
||||
'abbr',
|
||||
'article',
|
||||
'b',
|
||||
'blockquote',
|
||||
'br',
|
||||
'caption',
|
||||
'code',
|
||||
'del',
|
||||
'details',
|
||||
'div',
|
||||
'em',
|
||||
'h1',
|
||||
'h2',
|
||||
'h3',
|
||||
'h4',
|
||||
'h5',
|
||||
'h6',
|
||||
'hr',
|
||||
'i',
|
||||
'img',
|
||||
'ins',
|
||||
'kbd',
|
||||
'li',
|
||||
'main',
|
||||
'mark',
|
||||
'ol',
|
||||
'p',
|
||||
'pre',
|
||||
'section',
|
||||
'span',
|
||||
'strike',
|
||||
'strong',
|
||||
'sub',
|
||||
'summary',
|
||||
'sup',
|
||||
'table',
|
||||
'tbody',
|
||||
'td',
|
||||
'th',
|
||||
'thead',
|
||||
'tr',
|
||||
'u',
|
||||
'ul',
|
||||
],
|
||||
allowedAttributes: {
|
||||
'*': ['title', 'accesskey'],
|
||||
@@ -24,30 +66,33 @@ const insaneOptions = {
|
||||
code: ['class'],
|
||||
span: ['class', 'style'],
|
||||
},
|
||||
filter: node => {
|
||||
filter: (node) => {
|
||||
// allow hljs style
|
||||
const allowed = [
|
||||
[ 'code', /^hljs\W+language-(.*)$/ ],
|
||||
[ 'span', /^(hljs-.*)$/ ]
|
||||
['code', /^hljs\W+language-(.*)$/],
|
||||
['span', /^(hljs-.*)$/],
|
||||
]
|
||||
allowed.forEach(([ tag, reg ]) => {
|
||||
allowed.forEach(([tag, reg]) => {
|
||||
if (
|
||||
node.tag === tag
|
||||
&& !!node.attrs.class
|
||||
&& !(reg as RegExp).test(node.attrs.class)
|
||||
node.tag === tag &&
|
||||
!!node.attrs.class &&
|
||||
!(reg as RegExp).test(node.attrs.class)
|
||||
) {
|
||||
delete node.attrs.class
|
||||
}
|
||||
})
|
||||
|
||||
// allow <span> set color sty
|
||||
if (node.tag === 'span' && !!node.attrs.style
|
||||
&& !/^color:(\W+)?#[0-9a-f]{3,6};?$/i.test(node.attrs.style)) {
|
||||
if (
|
||||
node.tag === 'span' &&
|
||||
!!node.attrs.style &&
|
||||
!/^color:(\W+)?#[0-9a-f]{3,6};?$/i.test(node.attrs.style)
|
||||
) {
|
||||
delete node.attrs.style
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export function sanitize(content: string): string {
|
||||
|
||||
@@ -2,16 +2,20 @@ import type { NotifyLevel } from '~/types'
|
||||
import * as Utils from './utils'
|
||||
|
||||
/** 显示加载 */
|
||||
export function showLoading(parentElem: HTMLElement, conf?: { transparentBg?: boolean }) {
|
||||
export function showLoading(
|
||||
parentElem: HTMLElement,
|
||||
conf?: { transparentBg?: boolean },
|
||||
) {
|
||||
// Use :scope https://developer.mozilla.org/en-US/docs/Web/CSS/:scope
|
||||
let $loading = parentElem.querySelector<HTMLElement>(':scope > .atk-loading')
|
||||
if (!$loading) {
|
||||
$loading = Utils.createElement(
|
||||
`<div class="atk-loading atk-fade-in" style="display: none;">
|
||||
`<div class="atk-loading atk-fade-in" style="display: none;">
|
||||
<div class="atk-loading-spinner">
|
||||
<svg viewBox="25 25 50 50"><circle cx="50" cy="50" r="20" fill="none" stroke-width="2" stroke-miterlimit="10"></circle></svg>
|
||||
</div>
|
||||
</div>`)
|
||||
</div>`,
|
||||
)
|
||||
if (conf?.transparentBg) $loading.style.background = 'transparent'
|
||||
parentElem.appendChild($loading)
|
||||
}
|
||||
@@ -29,7 +33,9 @@ export function showLoading(parentElem: HTMLElement, conf?: { transparentBg?: bo
|
||||
|
||||
/** 隐藏加载 */
|
||||
export function hideLoading(parentElem: HTMLElement) {
|
||||
const $loading = parentElem.querySelector<HTMLElement>(':scope > .atk-loading')
|
||||
const $loading = parentElem.querySelector<HTMLElement>(
|
||||
':scope > .atk-loading',
|
||||
)
|
||||
if ($loading) $loading.style.display = 'none'
|
||||
}
|
||||
|
||||
@@ -40,7 +46,10 @@ export function setLoading(val: boolean, parentElem: HTMLElement) {
|
||||
}
|
||||
|
||||
/** 元素是否用户可见 */
|
||||
export function isVisible(el: HTMLElement, viewport: HTMLElement = document.documentElement) {
|
||||
export function isVisible(
|
||||
el: HTMLElement,
|
||||
viewport: HTMLElement = document.documentElement,
|
||||
) {
|
||||
const viewportHeight = viewport.clientHeight
|
||||
|
||||
const docViewTop = viewport.scrollTop
|
||||
@@ -50,11 +59,15 @@ export function isVisible(el: HTMLElement, viewport: HTMLElement = document.docu
|
||||
const elemTop = rect.top + docViewTop
|
||||
const elemBottom = elemTop + rect.height
|
||||
|
||||
return (elemBottom <= docViewBottom) //&& (elemTop >= docViewTop) 注释因为假如 el 比 viewport 还高就会失效
|
||||
return elemBottom <= docViewBottom //&& (elemTop >= docViewTop) 注释因为假如 el 比 viewport 还高就会失效
|
||||
}
|
||||
|
||||
/** 滚动到元素中心 */
|
||||
export function scrollIntoView(elem: HTMLElement, enableAnim: boolean = true, relativeTo?: HTMLElement) {
|
||||
export function scrollIntoView(
|
||||
elem: HTMLElement,
|
||||
enableAnim: boolean = true,
|
||||
relativeTo?: HTMLElement,
|
||||
) {
|
||||
let top: number
|
||||
|
||||
if (relativeTo) {
|
||||
@@ -69,9 +82,10 @@ export function scrollIntoView(elem: HTMLElement, enableAnim: boolean = true, re
|
||||
}
|
||||
|
||||
const scrollOptions: ScrollToOptions = {
|
||||
top, left: 0,
|
||||
top,
|
||||
left: 0,
|
||||
// behavior: enableAnim ? 'smooth' : 'instant',
|
||||
behavior: 'instant'
|
||||
behavior: 'instant',
|
||||
}
|
||||
|
||||
if (relativeTo) relativeTo.scroll(scrollOptions)
|
||||
@@ -88,9 +102,11 @@ export function showNotify(
|
||||
const timeout = 3000 // 持续显示时间 ms
|
||||
|
||||
const notifyElem = Utils.createElement(
|
||||
`<div class="atk-notify atk-fade-in" style="background-color: ${colors[type]}"><span class="atk-notify-content"></span></div>`
|
||||
`<div class="atk-notify atk-fade-in" style="background-color: ${colors[type]}"><span class="atk-notify-content"></span></div>`,
|
||||
)
|
||||
const notifyContentEl = notifyElem.querySelector<HTMLElement>('.atk-notify-content')!
|
||||
const notifyContentEl = notifyElem.querySelector<HTMLElement>(
|
||||
'.atk-notify-content',
|
||||
)!
|
||||
notifyContentEl.innerHTML = Utils.htmlEncode(msg).replace('\n', '<br/>')
|
||||
|
||||
wrapElem.appendChild(notifyElem)
|
||||
@@ -119,7 +135,7 @@ export function showNotify(
|
||||
export function playFadeAnim(
|
||||
elem: HTMLElement,
|
||||
after?: () => void,
|
||||
type: 'in' | 'out' = 'in'
|
||||
type: 'in' | 'out' = 'in',
|
||||
) {
|
||||
elem.classList.add(`atk-fade-${type}`)
|
||||
// 动画结束清除 class
|
||||
@@ -142,7 +158,11 @@ export function playFadeOutAnim(elem: HTMLElement, after?: () => void) {
|
||||
}
|
||||
|
||||
/** 设置全局错误 */
|
||||
export function setError(parentElem: HTMLElement, html: string | HTMLElement | null, title: string = '<span class="atk-error-title">Artalk Error</span>') {
|
||||
export function setError(
|
||||
parentElem: HTMLElement,
|
||||
html: string | HTMLElement | null,
|
||||
title: string = '<span class="atk-error-title">Artalk Error</span>',
|
||||
) {
|
||||
let elem = parentElem.querySelector<HTMLElement>('.atk-error-layer')
|
||||
if (html === null) {
|
||||
if (elem !== null) elem.remove()
|
||||
@@ -150,7 +170,7 @@ export function setError(parentElem: HTMLElement, html: string | HTMLElement | n
|
||||
}
|
||||
if (!elem) {
|
||||
elem = Utils.createElement(
|
||||
`<div class="atk-error-layer">${title}<span class="atk-error-text"></span></div>`
|
||||
`<div class="atk-error-layer">${title}<span class="atk-error-text"></span></div>`,
|
||||
)
|
||||
parentElem.appendChild(elem)
|
||||
}
|
||||
@@ -189,5 +209,5 @@ export function getScrollBarWidth() {
|
||||
|
||||
document.body.removeChild(outer)
|
||||
|
||||
return (w1 - w2)
|
||||
return w1 - w2
|
||||
}
|
||||
Loaded 100 of 179 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user