Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ba8a221833 | ||
|
|
3f73cd9443 | ||
|
|
7a0a9dbd92 | ||
|
|
441c4fa6a9 | ||
|
|
a16169f69d | ||
|
|
da42676656 | ||
|
|
f3a42e892f | ||
|
|
6a53597485 | ||
|
|
60336e567d | ||
|
|
8918c62ffd | ||
|
|
362fc19cd8 | ||
|
|
1b938752ff | ||
|
|
835a886dc8 | ||
|
|
f390b57fe7 | ||
|
|
6d21a0f204 | ||
|
|
a1d0c37828 | ||
|
|
763fbc417e | ||
|
|
776d5e66fe | ||
|
|
10c0975ce0 | ||
|
|
7286642fc9 | ||
|
|
8db4fb3524 | ||
|
|
08be04979d |
No files matched your search
@@ -10,7 +10,7 @@ executors:
|
||||
- image: cimg/base:stable
|
||||
node:
|
||||
docker:
|
||||
- image: 'cimg/node:18.17.1'
|
||||
- image: 'cimg/node:18.19.0'
|
||||
go:
|
||||
docker:
|
||||
- image: cimg/go:1.21.3
|
||||
@@ -19,9 +19,6 @@ parameters:
|
||||
trigger-app:
|
||||
type: boolean
|
||||
default: false
|
||||
trigger-ui:
|
||||
type: boolean
|
||||
default: false
|
||||
run-them-all:
|
||||
type: boolean
|
||||
default: false
|
||||
@@ -60,25 +57,6 @@ jobs:
|
||||
flags: go
|
||||
file: /tmp/test-reports/coverage.out
|
||||
|
||||
## Frontend
|
||||
test_ui:
|
||||
executor: node
|
||||
steps:
|
||||
- node/install
|
||||
- checkout
|
||||
- run:
|
||||
name: Print the node version
|
||||
command: node -v
|
||||
- run:
|
||||
name: Install pnpm
|
||||
command: npm i -g pnpm@8.7.0
|
||||
- run:
|
||||
name: Install Dependencies
|
||||
command: pnpm --dir ./ui install --frozen-lockfile
|
||||
- run:
|
||||
name: Build Artalk
|
||||
command: pnpm --dir ./ui build:all
|
||||
|
||||
all_projects:
|
||||
executor: base
|
||||
steps:
|
||||
@@ -95,14 +73,6 @@ workflows:
|
||||
jobs:
|
||||
- test_app
|
||||
|
||||
build-ui:
|
||||
when:
|
||||
or:
|
||||
- << pipeline.parameters.trigger-ui >>
|
||||
- << pipeline.parameters.run-them-all >>
|
||||
jobs:
|
||||
- test_ui
|
||||
|
||||
build-shared-other:
|
||||
when: << pipeline.parameters.run-them-all >>
|
||||
jobs:
|
||||
|
||||
@@ -17,7 +17,6 @@ workflows:
|
||||
mapping: |
|
||||
(cmd|internal|server|pkg|test|conf)/.* trigger-app true
|
||||
main.go|go.mod|go.sum trigger-app true
|
||||
(ui)/.* trigger-ui true
|
||||
base-revision: master # git branch name
|
||||
config-path: .circleci/conditional_config.yml
|
||||
filters:
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
name: Test Frontend
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- 'ui/**'
|
||||
pull_request:
|
||||
paths:
|
||||
- 'ui/**'
|
||||
|
||||
jobs:
|
||||
test_ui:
|
||||
strategy:
|
||||
matrix:
|
||||
node: [18, 20]
|
||||
os: [ubuntu-latest, windows-latest]
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./ui
|
||||
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 8
|
||||
|
||||
- name: Use Node.js ${{ matrix.node }}
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
cache: pnpm
|
||||
cache-dependency-path: './ui/pnpm-lock.yaml'
|
||||
|
||||
- name: Install Dependencies
|
||||
run: pnpm --dir ./ui install --frozen-lockfile
|
||||
|
||||
- name: Package Build Test
|
||||
run: pnpm build:all
|
||||
|
||||
# TODO: Add unit test
|
||||
# - name: Unit test
|
||||
# run: pnpm test
|
||||
@@ -1,4 +1,17 @@
|
||||
|
||||
## [v2.7.1](https://github.com/ArtalkJS/Artalk/compare/v2.7.0...v2.7.1) (2023-12-17)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **build:** generate source maps to ease debugging
|
||||
* **cout-widget:** `loadCountWidget` issue
|
||||
* **ui:** static method export issue in JS module ([#669](https://github.com/ArtalkJS/Artalk/issues/669))
|
||||
|
||||
### Documentation
|
||||
|
||||
* **i18n:** add translation of update notice
|
||||
|
||||
|
||||
## [v2.7.0](https://github.com/ArtalkJS/Artalk/compare/v2.6.4...v2.7.0) (2023-12-17)
|
||||
|
||||
### Features
|
||||
|
||||
+11
-11
@@ -17,23 +17,23 @@ diverse, inclusive, and healthy community.
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
* Demonstrating empathy and kindness toward other people
|
||||
* Being respectful of differing opinions, viewpoints, and experiences
|
||||
* Giving and gracefully accepting constructive feedback
|
||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
- Demonstrating empathy and kindness toward other people
|
||||
- Being respectful of differing opinions, viewpoints, and experiences
|
||||
- Giving and gracefully accepting constructive feedback
|
||||
- Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
* Focusing on what is best not just for us as individuals, but for the overall
|
||||
- Focusing on what is best not just for us as individuals, but for the overall
|
||||
community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
* The use of sexualized language or imagery, and sexual attention or advances of
|
||||
- The use of sexualized language or imagery, and sexual attention or advances of
|
||||
any kind
|
||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or email address,
|
||||
- Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
- Public or private harassment
|
||||
- Publishing others' private information, such as a physical or email address,
|
||||
without their explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
- Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
@@ -59,7 +59,7 @@ representative at an online or offline event.
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement at artalkjs@gmail.com.
|
||||
reported to the community leaders responsible for enforcement at <artalkjs@gmail.com>.
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ This guide is for developers who want to contribute to the project.
|
||||
|
||||
To develop Artalk, including the frontend and backend, you need to install the following tools:
|
||||
|
||||
- [Node.js](https://nodejs.org/en/) (>= 16.0.0)
|
||||
- [Node.js](https://nodejs.org/en/) (>= 18.12.0)
|
||||
- [Go](https://golang.org/) (>= 1.21)
|
||||
- [Docker](https://www.docker.com/) (>= 20.10.0) (optional)
|
||||
- [Docker Compose](https://docs.docker.com/compose/) (>= 1.29.0) (optional)
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ WORKDIR /source
|
||||
# install tools
|
||||
RUN set -ex \
|
||||
&& apk add --no-cache make git gcc musl-dev nodejs bash npm\
|
||||
&& npm install -g pnpm@8.7.0
|
||||
&& npm install -g pnpm@8.12.1
|
||||
|
||||
# download go deps
|
||||
# (cache by separating the downloading of deps)
|
||||
|
||||
+4
-4
@@ -4,12 +4,12 @@
|
||||
|
||||
# Artalk
|
||||
|
||||
[](https://www.npmjs.com/package/artalk)
|
||||
[](https://www.npmjs.com/package/artalk)
|
||||
[](https://bundlephobia.com/package/artalk)
|
||||
[](https://www.npmjs.com/package/artalk)
|
||||
[](https://www.npmjs.com/package/artalk)
|
||||
[](https://bundlephobia.com/package/artalk)
|
||||
[](https://circleci.com/gh/ArtalkJS/Artalk/tree/master)
|
||||
|
||||
> 🌌 A Self-hosted comment system
|
||||
> 🌌 A self-hosted comment system
|
||||
|
||||
[简体中文](./README.md) / [Documentation](https://artalk.js.org) / [Releases](https://github.com/ArtalkJS/Artalk/releases) / [Artalk](https://github.com/ArtalkJS/Artalk)
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
|
||||
# Artalk
|
||||
|
||||
[](https://www.npmjs.com/package/artalk)
|
||||
[](https://www.npmjs.com/package/artalk)
|
||||
[](https://www.npmjs.com/package/artalk)
|
||||
[](https://www.npmjs.com/package/artalk)
|
||||
[](https://hub.docker.com/r/artalk/artalk-go)
|
||||
[](https://goreportcard.com/report/github.com/ArtalkJS/Artalk)
|
||||
[](https://circleci.com/gh/ArtalkJS/Artalk/tree/master)
|
||||
@@ -129,7 +129,7 @@ see [CONTRIBUTING.md](./CONTRIBUTING.md)
|
||||
|
||||
感谢社区提供的帮助与反馈,若有好的建议与意见欢迎前往 [ISSUES](https://github.com/ArtalkJS/Artalk/issues) 随时告知。
|
||||
|
||||
## TODOs
|
||||
## TODOs
|
||||
|
||||
- [x] Golang 后端
|
||||
- [x] 多数据库支持
|
||||
|
||||
+2
-2
@@ -26,7 +26,7 @@ var Banner = `
|
||||
|
||||
Artalk (` + Version + `)
|
||||
|
||||
-> A Selfhosted Comment System.
|
||||
-> A Self-hosted Comment System.
|
||||
-> https://artalk.js.org
|
||||
`
|
||||
|
||||
@@ -50,7 +50,7 @@ func New() *ArtalkCmd {
|
||||
cmd := &ArtalkCmd{
|
||||
RootCmd: &cobra.Command{
|
||||
Use: "artalk",
|
||||
Short: "Artalk: Your self-hosted comment system",
|
||||
Short: "Artalk: A self-hosted comment system",
|
||||
Long: Banner,
|
||||
Version: Version,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"latest": "2.7.0"
|
||||
"latest": "2.7.1"
|
||||
}
|
||||
@@ -12,7 +12,7 @@ npm install artalk
|
||||
|
||||
## 创建实例 `init`
|
||||
|
||||
`Artalk.init` 函数和 `new Artalk` 等价,调用该函数将创建并得到一个 Artalk 的实例化对象,可用于后续操作。
|
||||
调用该函数将创建并得到一个 Artalk 的实例化对象,可用于后续操作。
|
||||
|
||||
```js
|
||||
import Artalk from 'artalk'
|
||||
|
||||
+2
-3
@@ -2,11 +2,11 @@
|
||||
layout: home
|
||||
|
||||
title: Artalk
|
||||
titleTemplate: The Selfhosted Comment System
|
||||
titleTemplate: A Self-hosted Comment System
|
||||
|
||||
hero:
|
||||
name: Artalk
|
||||
text: The Selfhosted Comment System
|
||||
text: A Self-hosted Comment System
|
||||
tagline: Elevate your online conversations with a comment system that embodies the trifecta of lightness, security, and refinement.
|
||||
actions:
|
||||
- theme: brand
|
||||
@@ -30,4 +30,3 @@ features:
|
||||
title: Flexible
|
||||
details: Use Golang to interpret the excellence of multiple platforms
|
||||
---
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ titleTemplate: 自托管评论系统
|
||||
|
||||
hero:
|
||||
name: Artalk
|
||||
text: The Selfhosted Comment System
|
||||
text: A Self-hosted Comment System
|
||||
tagline: 轻量、安全、易上手的自托管评论系统
|
||||
actions:
|
||||
- theme: brand
|
||||
|
||||
@@ -8,8 +8,8 @@ export const Slogan: React.FC = () => {
|
||||
<div className="slogan-inner">
|
||||
<div className="text">
|
||||
<Reveal>
|
||||
The <span className='highlight'>
|
||||
Selfhosted
|
||||
A <span className='highlight'>
|
||||
Self-hosted
|
||||
<svg className='line-wrap' width="361" height="55" viewBox="0 0 361 55" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path className='line' d="M6 49C39.3707 29.6297 79.037 17.9573 117.534 28.5185C123.875 30.2581 129.86 32.8008 135.527 36.1667C139.986 38.8153 144.204 42.404 149.501 43.037C159.524 44.2349 168.187 33.3778 176.108 28.6481C191.347 19.5481 210.215 16.0215 227.536 21.1296C239.243 24.5822 249.671 29.5512 261.991 30.9815C295.794 34.9057 323.045 15.5757 354 7" stroke="#BDCFFF" stroke-width="12" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
||||
+1
-1
@@ -25,6 +25,6 @@
|
||||
"vitepress": "1.0.0-rc.14"
|
||||
},
|
||||
"dependencies": {
|
||||
"artalk": "^2.6.4"
|
||||
"artalk": "^2.7.0"
|
||||
}
|
||||
}
|
||||
Generated
+4
-4
@@ -6,8 +6,8 @@ settings:
|
||||
|
||||
dependencies:
|
||||
artalk:
|
||||
specifier: ^2.6.4
|
||||
version: 2.6.4
|
||||
specifier: ^2.7.0
|
||||
version: 2.7.0
|
||||
|
||||
devDependencies:
|
||||
'@redocly/cli':
|
||||
@@ -2074,8 +2074,8 @@ packages:
|
||||
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
|
||||
dev: true
|
||||
|
||||
/artalk@2.6.4:
|
||||
resolution: {integrity: sha512-26mnU1gL7aoM2g5i0AkgPgwoQ7+xvbkqbs9xs+ZFdT6BMeyC1lpMVIPLLui7VIRtJnJAq6sUQxMb4Lq6F2AVZA==}
|
||||
/artalk@2.7.0:
|
||||
resolution: {integrity: sha512-eV25c1vw0GtGmobh3W2bCjlIKxEd+k3u7WQ0IgC1ULd9PYCdwRMKjN7K+W+qlkUMVm2Kf4ti0Luouu07aE3sIw==}
|
||||
dependencies:
|
||||
abortcontroller-polyfill: 1.7.5
|
||||
hanabi: 0.4.0
|
||||
|
||||
+2
-2
@@ -4,9 +4,9 @@ extends:
|
||||
- recommended
|
||||
|
||||
apis:
|
||||
artalk@v1:
|
||||
artalk@v2:
|
||||
root: ./swagger/swagger.json
|
||||
rules: []
|
||||
rules: {}
|
||||
|
||||
theme:
|
||||
openapi:
|
||||
|
||||
@@ -150,6 +150,21 @@ func (conf *Config) i18nPatch() {
|
||||
} else if conf.Locale == "zh" {
|
||||
conf.Locale = "zh-CN"
|
||||
}
|
||||
|
||||
// Case Convert
|
||||
// follow Unicode BCP 47
|
||||
// @see https://www.techonthenet.com/js/language_tags.php
|
||||
parts := strings.Split(conf.Locale, "-")
|
||||
if len(parts) == 2 {
|
||||
conf.Locale = strings.ToLower(parts[0]) + "-" + strings.ToUpper(parts[1])
|
||||
} else if len(parts) == 1 {
|
||||
conf.Locale = strings.ToLower(parts[0])
|
||||
}
|
||||
|
||||
// Temporary convert `en-*` to `en`
|
||||
if parts[0] == "en" {
|
||||
conf.Locale = "en"
|
||||
}
|
||||
}
|
||||
|
||||
// IP属地功能配置修补
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestI18nPatch(t *testing.T) {
|
||||
test := func(input string, expected string) {
|
||||
t.Run("locale="+input, func(t *testing.T) {
|
||||
config := &Config{Locale: input}
|
||||
config.i18nPatch()
|
||||
assert.Equal(t, expected, config.Locale)
|
||||
})
|
||||
}
|
||||
|
||||
test("", "en")
|
||||
test("en", "en")
|
||||
test("en-US", "en")
|
||||
|
||||
test("zh", "zh-CN")
|
||||
test("zh-CN", "zh-CN")
|
||||
|
||||
t.Run("case convert", func(t *testing.T) {
|
||||
// @see https://www.techonthenet.com/js/language_tags.php
|
||||
test("zh-cn", "zh-CN")
|
||||
test("ZH-cn", "zh-CN")
|
||||
test("EN", "en")
|
||||
})
|
||||
}
|
||||
@@ -11,8 +11,8 @@ then
|
||||
export VOLTA_HOME="${HOME}/.volta"
|
||||
export PATH="${VOLTA_HOME}/bin:${PATH}"
|
||||
|
||||
volta install node@18.17.1
|
||||
volta install pnpm@8.7.0
|
||||
volta install node@18.19.0
|
||||
volta install pnpm@8.12.1
|
||||
fi
|
||||
|
||||
pnpm --dir ./ui install --frozen-lockfile
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
/.vitepress/dist/
|
||||
/.vitepress/.temp/
|
||||
/.vitepress/cache/
|
||||
/public/http-api.html
|
||||
@@ -0,0 +1,28 @@
|
||||
import { defineConfig } from 'vitepress'
|
||||
|
||||
// https://vitepress.dev/reference/site-config
|
||||
export default defineConfig({
|
||||
title: "My Awesome Project",
|
||||
description: "A VitePress Site",
|
||||
themeConfig: {
|
||||
// https://vitepress.dev/reference/default-theme-config
|
||||
nav: [
|
||||
{ text: 'Home', link: '/' },
|
||||
{ text: 'Examples', link: '/markdown-examples' }
|
||||
],
|
||||
|
||||
sidebar: [
|
||||
{
|
||||
text: 'Examples',
|
||||
items: [
|
||||
{ text: 'Markdown Examples', link: '/markdown-examples' },
|
||||
{ text: 'Runtime API Examples', link: '/api-examples' }
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
socialLinks: [
|
||||
{ icon: 'github', link: 'https://github.com/vuejs/vitepress' }
|
||||
]
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,80 @@
|
||||
<template>
|
||||
<div ref="el" style="margin-top: 20px;"></div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { watch, nextTick, ref, onMounted, onUnmounted } from 'vue'
|
||||
import { useData, useRouter } from 'vitepress'
|
||||
import Artalk from 'artalk'
|
||||
import 'artalk/dist/artalk.css'
|
||||
|
||||
Artalk.use((ctx) => {
|
||||
console.log(ctx)
|
||||
})
|
||||
|
||||
const el = ref<HTMLElement>()
|
||||
|
||||
const router = useRouter()
|
||||
const page = useData().page
|
||||
|
||||
let artalk: Artalk
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
initArtalk(getConfByPage())
|
||||
})
|
||||
})
|
||||
|
||||
watch(() => router.route.path, () => {
|
||||
nextTick(() => {
|
||||
artalk.update(getConfByPage())
|
||||
artalk.reload()
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
artalk.destroy()
|
||||
})
|
||||
|
||||
function initArtalk(conf: any) {
|
||||
artalk = Artalk.init({
|
||||
el: el.value,
|
||||
emoticons: '/assets/emoticons/default.json',
|
||||
gravatar: {
|
||||
mirror: 'https://cravatar.cn/avatar/'
|
||||
},
|
||||
...conf
|
||||
})
|
||||
|
||||
loadExtraFuncs()
|
||||
}
|
||||
|
||||
function getConfByPage() {
|
||||
return {
|
||||
pageKey: 'https://artalk.js.org'+router.route.path,
|
||||
pageTitle: page.value.title,
|
||||
server: 'http://localhost:23366',
|
||||
site: 'ArtalkDocs',
|
||||
}
|
||||
}
|
||||
|
||||
function loadExtraFuncs() {
|
||||
// 夜间模式
|
||||
const darkMode = document.querySelector('html')!.classList.contains('dark')
|
||||
artalk.setDarkMode(darkMode)
|
||||
|
||||
new MutationObserver((mList) => {
|
||||
mList.forEach((m) => {
|
||||
if (m.attributeName !== 'class') return
|
||||
|
||||
// @ts-ignore
|
||||
const darkMode = m.target.classList.contains('dark')
|
||||
artalk.setDarkMode(darkMode)
|
||||
})
|
||||
}).observe(document.querySelector('html')!, { attributes: true })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,12 @@
|
||||
<script lang="ts" setup>
|
||||
import { onMounted } from 'vue'
|
||||
import Artalk from './Artalk.vue';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Artalk />
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,19 @@
|
||||
<script setup>
|
||||
import DefaultTheme from 'vitepress/theme'
|
||||
import { onMounted } from 'vue'
|
||||
import Artalk from './Artalk.vue'
|
||||
import './style.scss'
|
||||
|
||||
const { Layout } = DefaultTheme
|
||||
|
||||
onMounted(() => {
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Layout>
|
||||
<template #doc-after>
|
||||
<Artalk />
|
||||
</template>
|
||||
</Layout>
|
||||
</template>
|
||||
@@ -0,0 +1,19 @@
|
||||
// https://vitepress.dev/guide/custom-theme
|
||||
import { h } from 'vue'
|
||||
import type { Theme } from 'vitepress'
|
||||
import DefaultTheme from 'vitepress/theme'
|
||||
import Artalk from './Artalk.vue'
|
||||
import './style.css'
|
||||
|
||||
export default {
|
||||
extends: DefaultTheme,
|
||||
Layout: () => {
|
||||
return h(DefaultTheme.Layout, null, {
|
||||
'doc-after': () => h(Artalk)
|
||||
// https://vitepress.dev/guide/extending-default-theme#layout-slots
|
||||
})
|
||||
},
|
||||
enhanceApp({ app, router, siteData }) {
|
||||
// ...
|
||||
}
|
||||
} satisfies Theme
|
||||
@@ -0,0 +1,139 @@
|
||||
/**
|
||||
* Customize default theme styling by overriding CSS variables:
|
||||
* https://github.com/vuejs/vitepress/blob/main/src/client/theme-default/styles/vars.css
|
||||
*/
|
||||
|
||||
/**
|
||||
* Colors
|
||||
*
|
||||
* Each colors have exact same color scale system with 3 levels of solid
|
||||
* colors with different brightness, and 1 soft color.
|
||||
*
|
||||
* - `XXX-1`: The most solid color used mainly for colored text. It must
|
||||
* satisfy the contrast ratio against when used on top of `XXX-soft`.
|
||||
*
|
||||
* - `XXX-2`: The color used mainly for hover state of the button.
|
||||
*
|
||||
* - `XXX-3`: The color for solid background, such as bg color of the button.
|
||||
* It must satisfy the contrast ratio with pure white (#ffffff) text on
|
||||
* top of it.
|
||||
*
|
||||
* - `XXX-soft`: The color used for subtle background such as custom container
|
||||
* or badges. It must satisfy the contrast ratio when putting `XXX-1` colors
|
||||
* on top of it.
|
||||
*
|
||||
* The soft color must be semi transparent alpha channel. This is crucial
|
||||
* because it allows adding multiple "soft" colors on top of each other
|
||||
* to create a accent, such as when having inline code block inside
|
||||
* custom containers.
|
||||
*
|
||||
* - `default`: The color used purely for subtle indication without any
|
||||
* special meanings attched to it such as bg color for menu hover state.
|
||||
*
|
||||
* - `brand`: Used for primary brand colors, such as link text, button with
|
||||
* brand theme, etc.
|
||||
*
|
||||
* - `tip`: Used to indicate useful information. The default theme uses the
|
||||
* brand color for this by default.
|
||||
*
|
||||
* - `warning`: Used to indicate warning to the users. Used in custom
|
||||
* container, badges, etc.
|
||||
*
|
||||
* - `danger`: Used to show error, or dangerous message to the users. Used
|
||||
* in custom container, badges, etc.
|
||||
* -------------------------------------------------------------------------- */
|
||||
|
||||
:root {
|
||||
--vp-c-default-1: var(--vp-c-gray-1);
|
||||
--vp-c-default-2: var(--vp-c-gray-2);
|
||||
--vp-c-default-3: var(--vp-c-gray-3);
|
||||
--vp-c-default-soft: var(--vp-c-gray-soft);
|
||||
|
||||
--vp-c-brand-1: var(--vp-c-indigo-1);
|
||||
--vp-c-brand-2: var(--vp-c-indigo-2);
|
||||
--vp-c-brand-3: var(--vp-c-indigo-3);
|
||||
--vp-c-brand-soft: var(--vp-c-indigo-soft);
|
||||
|
||||
--vp-c-tip-1: var(--vp-c-brand-1);
|
||||
--vp-c-tip-2: var(--vp-c-brand-2);
|
||||
--vp-c-tip-3: var(--vp-c-brand-3);
|
||||
--vp-c-tip-soft: var(--vp-c-brand-soft);
|
||||
|
||||
--vp-c-warning-1: var(--vp-c-yellow-1);
|
||||
--vp-c-warning-2: var(--vp-c-yellow-2);
|
||||
--vp-c-warning-3: var(--vp-c-yellow-3);
|
||||
--vp-c-warning-soft: var(--vp-c-yellow-soft);
|
||||
|
||||
--vp-c-danger-1: var(--vp-c-red-1);
|
||||
--vp-c-danger-2: var(--vp-c-red-2);
|
||||
--vp-c-danger-3: var(--vp-c-red-3);
|
||||
--vp-c-danger-soft: var(--vp-c-red-soft);
|
||||
}
|
||||
|
||||
/**
|
||||
* Component: Button
|
||||
* -------------------------------------------------------------------------- */
|
||||
|
||||
:root {
|
||||
--vp-button-brand-border: transparent;
|
||||
--vp-button-brand-text: var(--vp-c-white);
|
||||
--vp-button-brand-bg: var(--vp-c-brand-3);
|
||||
--vp-button-brand-hover-border: transparent;
|
||||
--vp-button-brand-hover-text: var(--vp-c-white);
|
||||
--vp-button-brand-hover-bg: var(--vp-c-brand-2);
|
||||
--vp-button-brand-active-border: transparent;
|
||||
--vp-button-brand-active-text: var(--vp-c-white);
|
||||
--vp-button-brand-active-bg: var(--vp-c-brand-1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Component: Home
|
||||
* -------------------------------------------------------------------------- */
|
||||
|
||||
:root {
|
||||
--vp-home-hero-name-color: transparent;
|
||||
--vp-home-hero-name-background: -webkit-linear-gradient(
|
||||
120deg,
|
||||
#bd34fe 30%,
|
||||
#41d1ff
|
||||
);
|
||||
|
||||
--vp-home-hero-image-background-image: linear-gradient(
|
||||
-45deg,
|
||||
#bd34fe 50%,
|
||||
#47caff 50%
|
||||
);
|
||||
--vp-home-hero-image-filter: blur(44px);
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
:root {
|
||||
--vp-home-hero-image-filter: blur(56px);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
:root {
|
||||
--vp-home-hero-image-filter: blur(68px);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Component: Custom Block
|
||||
* -------------------------------------------------------------------------- */
|
||||
|
||||
:root {
|
||||
--vp-custom-block-tip-border: transparent;
|
||||
--vp-custom-block-tip-text: var(--vp-c-text-1);
|
||||
--vp-custom-block-tip-bg: var(--vp-c-brand-soft);
|
||||
--vp-custom-block-tip-code-bg: var(--vp-c-brand-soft);
|
||||
}
|
||||
|
||||
/**
|
||||
* Component: Algolia
|
||||
* -------------------------------------------------------------------------- */
|
||||
|
||||
.DocSearch {
|
||||
--docsearch-primary-color: var(--vp-c-brand-1) !important;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
---
|
||||
outline: deep
|
||||
---
|
||||
|
||||
# Runtime API Examples
|
||||
|
||||
This page demonstrates usage of some of the runtime APIs provided by VitePress.
|
||||
|
||||
The main `useData()` API can be used to access site, theme, and page data for the current page. It works in both `.md` and `.vue` files:
|
||||
|
||||
```md
|
||||
<script setup>
|
||||
import { useData } from 'vitepress'
|
||||
|
||||
const { theme, page, frontmatter } = useData()
|
||||
</script>
|
||||
|
||||
## Results
|
||||
|
||||
### Theme Data
|
||||
<pre>{{ theme }}</pre>
|
||||
|
||||
### Page Data
|
||||
<pre>{{ page }}</pre>
|
||||
|
||||
### Page Frontmatter
|
||||
<pre>{{ frontmatter }}</pre>
|
||||
```
|
||||
|
||||
<script setup>
|
||||
import { useData } from 'vitepress'
|
||||
|
||||
const { site, theme, page, frontmatter } = useData()
|
||||
</script>
|
||||
|
||||
## Results
|
||||
|
||||
### Theme Data
|
||||
<pre>{{ theme }}</pre>
|
||||
|
||||
### Page Data
|
||||
<pre>{{ page }}</pre>
|
||||
|
||||
### Page Frontmatter
|
||||
<pre>{{ frontmatter }}</pre>
|
||||
|
||||
## More
|
||||
|
||||
Check out the documentation for the [full list of runtime APIs](https://vitepress.dev/reference/runtime-api#usedata).
|
||||
@@ -0,0 +1,25 @@
|
||||
---
|
||||
# https://vitepress.dev/reference/default-theme-home-page
|
||||
layout: home
|
||||
|
||||
hero:
|
||||
name: "My Awesome Project"
|
||||
text: "A VitePress Site"
|
||||
tagline: My great project tagline
|
||||
actions:
|
||||
- theme: brand
|
||||
text: Markdown Examples
|
||||
link: /markdown-examples
|
||||
- theme: alt
|
||||
text: API Examples
|
||||
link: /api-examples
|
||||
|
||||
features:
|
||||
- title: Feature A
|
||||
details: Lorem ipsum dolor sit amet, consectetur adipiscing elit
|
||||
- title: Feature B
|
||||
details: Lorem ipsum dolor sit amet, consectetur adipiscing elit
|
||||
- title: Feature C
|
||||
details: Lorem ipsum dolor sit amet, consectetur adipiscing elit
|
||||
---
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
# Markdown Extension Examples
|
||||
|
||||
This page demonstrates some of the built-in markdown extensions provided by VitePress.
|
||||
|
||||
## Syntax Highlighting
|
||||
|
||||
VitePress provides Syntax Highlighting powered by [Shikiji](https://github.com/antfu/shikiji), with additional features like line-highlighting:
|
||||
|
||||
**Input**
|
||||
|
||||
````md
|
||||
```js{4}
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
msg: 'Highlighted!'
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
````
|
||||
|
||||
**Output**
|
||||
|
||||
```js{4}
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
msg: 'Highlighted!'
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Custom Containers
|
||||
|
||||
**Input**
|
||||
|
||||
```md
|
||||
::: info
|
||||
This is an info box.
|
||||
:::
|
||||
|
||||
::: tip
|
||||
This is a tip.
|
||||
:::
|
||||
|
||||
::: warning
|
||||
This is a warning.
|
||||
:::
|
||||
|
||||
::: danger
|
||||
This is a dangerous warning.
|
||||
:::
|
||||
|
||||
::: details
|
||||
This is a details block.
|
||||
:::
|
||||
```
|
||||
|
||||
**Output**
|
||||
|
||||
::: info
|
||||
This is an info box.
|
||||
:::
|
||||
|
||||
::: tip
|
||||
This is a tip.
|
||||
:::
|
||||
|
||||
::: warning
|
||||
This is a warning.
|
||||
:::
|
||||
|
||||
::: danger
|
||||
This is a dangerous warning.
|
||||
:::
|
||||
|
||||
::: details
|
||||
This is a details block.
|
||||
:::
|
||||
|
||||
## More
|
||||
|
||||
Check out the documentation for the [full list of markdown extensions](https://vitepress.dev/guide/markdown).
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "ssg-test",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"docs:dev": "vitepress dev",
|
||||
"docs:build": "vitepress build",
|
||||
"docs:preview": "vitepress preview"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"vitepress": "1.0.0-rc.32"
|
||||
},
|
||||
"dependencies": {
|
||||
"artalk": "link:ui/packages/artalk",
|
||||
"vue": "^3.3.12"
|
||||
}
|
||||
}
|
||||
Generated
+1249
File diff suppressed because it is too large.
Load diff
@@ -9,10 +9,6 @@ const route = useRoute()
|
||||
|
||||
let artalk: Artalk
|
||||
|
||||
Artalk.use((ctx) => {
|
||||
ctx.fetch
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
artalk = Artalk.init({
|
||||
el: el.value,
|
||||
|
||||
@@ -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
|
||||
+12
-9
@@ -1,7 +1,9 @@
|
||||
const path = require('node:path')
|
||||
|
||||
module.exports = {
|
||||
root: true,
|
||||
parser: "@typescript-eslint/parser",
|
||||
env: { "browser": true },
|
||||
parser: '@typescript-eslint/parser',
|
||||
env: { browser: true },
|
||||
parserOptions: {
|
||||
project: ['./packages/*/tsconfig.json'],
|
||||
tsconfigRootDir: __dirname,
|
||||
@@ -12,9 +14,10 @@ module.exports = {
|
||||
'prettier',
|
||||
'plugin:import/recommended',
|
||||
'plugin:import/typescript',
|
||||
"plugin:compat/recommended",
|
||||
'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,
|
||||
@@ -38,11 +41,11 @@ module.exports = {
|
||||
settings: {
|
||||
'import/resolver': {
|
||||
typescript: {
|
||||
"project": [__dirname + "/packages/*/tsconfig.json"]
|
||||
project: ['packages/artalk/tsconfig.json'].map((p) =>
|
||||
path.resolve(__dirname, p),
|
||||
),
|
||||
},
|
||||
},
|
||||
'polyfills': [
|
||||
'AbortController'
|
||||
]
|
||||
}
|
||||
polyfills: ['AbortController'],
|
||||
},
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
+29
-22
@@ -7,34 +7,41 @@
|
||||
"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.39.0",
|
||||
"@rollup/plugin-dynamic-import-vars": "2.0.5",
|
||||
"@types/node": "20.5.2",
|
||||
"@typescript-eslint/eslint-plugin": "6.4.1",
|
||||
"@typescript-eslint/parser": "6.4.1",
|
||||
"autoprefixer": "10.4.15",
|
||||
"eslint": "8.47.0",
|
||||
"@playwright/test": "^1.40.1",
|
||||
"@rollup/plugin-dynamic-import-vars": "2.1.2",
|
||||
"@types/node": "20.10.5",
|
||||
"@typescript-eslint/eslint-plugin": "6.15.0",
|
||||
"@typescript-eslint/parser": "6.15.0",
|
||||
"autoprefixer": "10.4.16",
|
||||
"eslint": "8.56.0",
|
||||
"eslint-config-airbnb-base": "15.0.0",
|
||||
"eslint-config-airbnb-typescript": "17.1.0",
|
||||
"eslint-config-prettier": "9.0.0",
|
||||
"eslint-import-resolver-typescript": "2.7.0",
|
||||
"eslint-plugin-compat": "4.1.4",
|
||||
"eslint-plugin-import": "2.28.1",
|
||||
"postcss": "8.4.28",
|
||||
"prettier": "3.0.2",
|
||||
"rollup": "3.28.1",
|
||||
"sass": "1.66.1",
|
||||
"terser": "5.19.2",
|
||||
"typescript": "5.1.6",
|
||||
"vite": "4.4.9",
|
||||
"eslint-config-prettier": "9.1.0",
|
||||
"eslint-import-resolver-typescript": "3.6.1",
|
||||
"eslint-plugin-compat": "4.2.0",
|
||||
"eslint-plugin-import": "2.29.1",
|
||||
"postcss": "8.4.32",
|
||||
"prettier": "3.1.1",
|
||||
"rollup": "4.9.1",
|
||||
"sass": "1.69.5",
|
||||
"terser": "5.26.0",
|
||||
"typescript": "5.3.3",
|
||||
"vite": "5.0.10",
|
||||
"vite-plugin-checker": "0.6.2",
|
||||
"vite-plugin-dts": "3.5.2",
|
||||
"vite-tsconfig-paths": "4.2.0"
|
||||
"vite-plugin-dts": "3.6.4",
|
||||
"vite-tsconfig-paths": "4.2.2"
|
||||
},
|
||||
"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" />
|
||||
|
||||
@@ -12,20 +12,20 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"artalk": "workspace:^",
|
||||
"pinia": "^2.1.6",
|
||||
"vue": "^3.3.4",
|
||||
"vue-i18n": "^9.2.2",
|
||||
"vue-router": "^4.2.4",
|
||||
"yaml": "^2.3.1"
|
||||
"pinia": "^2.1.7",
|
||||
"vue": "^3.3.12",
|
||||
"vue-i18n": "^9.8.0",
|
||||
"vue-router": "^4.2.5",
|
||||
"yaml": "^2.3.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^4.3.3",
|
||||
"@vue/tsconfig": "^0.4.0",
|
||||
"typescript": "^5.1.6",
|
||||
"unplugin-auto-import": "^0.16.6",
|
||||
"unplugin-vue-components": "^0.25.1",
|
||||
"unplugin-vue-router": "^0.6.4",
|
||||
"vite": "^4.4.9",
|
||||
"vue-tsc": "^1.8.8"
|
||||
"@vitejs/plugin-vue": "^4.5.2",
|
||||
"@vue/tsconfig": "^0.5.1",
|
||||
"typescript": "^5.3.3",
|
||||
"unplugin-auto-import": "^0.17.2",
|
||||
"unplugin-vue-components": "^0.26.0",
|
||||
"unplugin-vue-router": "^0.7.0",
|
||||
"vite": "^5.0.10",
|
||||
"vue-tsc": "^1.8.25"
|
||||
}
|
||||
}
|
||||
@@ -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,15 +41,16 @@ 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').data.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').data
|
||||
const userData = artalk.ctx.get('user').getData()
|
||||
useUserStore().$patch((state) => {
|
||||
state.site = '__ATK_SITE_ALL'
|
||||
state.name = userData.nick
|
||||
|
||||
@@ -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";`,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -1,7 +0,0 @@
|
||||
/*
|
||||
!dist/
|
||||
!src/
|
||||
!types/
|
||||
!package.json
|
||||
!README.md
|
||||
!LICENSE
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
# Artalk
|
||||
|
||||
> The Selfhosted Comment System
|
||||
> A Self-hosted Comment System
|
||||
|
||||
## Links
|
||||
|
||||
|
||||
+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></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>
|
||||
@@ -1,8 +1,7 @@
|
||||
{
|
||||
"name": "artalk",
|
||||
"version": "2.7.0",
|
||||
"license": "MIT",
|
||||
"description": "A Selfhosted Comment System",
|
||||
"version": "2.7.1",
|
||||
"description": "A self-hosted comment system",
|
||||
"keywords": [
|
||||
"comment-system",
|
||||
"comments-widget",
|
||||
@@ -10,40 +9,67 @@
|
||||
"comments",
|
||||
"blog"
|
||||
],
|
||||
"homepage": "https://artalk.js.org",
|
||||
"bugs": "https://github.com/ArtalkJS/Artalk/issue",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/ArtalkJS/Artalk.git",
|
||||
"directory": "ui/packages/artalk"
|
||||
},
|
||||
"homepage": "https://artalk.js.org",
|
||||
"bugs": "https://github.com/ArtalkJS/Artalk/issue",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/src/main.d.ts",
|
||||
"require": "./dist/Artalk.js",
|
||||
"default": "./dist/Artalk.es.js"
|
||||
},
|
||||
"./dist/*": "./dist/*",
|
||||
"./dist/Artalk": "./dist/Artalk.js",
|
||||
"./dist/Artalk.es": "./dist/Artalk.es.js",
|
||||
"./dist/Artalk.iife": "./dist/Artalk.iife.js",
|
||||
"./dist/ArtalkLite": "./dist/ArtalkLite.js",
|
||||
"./dist/ArtalkLite.es": "./dist/ArtalkLite.es.js",
|
||||
"./dist/ArtalkLite.iife": "./dist/ArtalkLite.iife.js",
|
||||
"./dist/i18n/jp": "./dist/i18n/jp.js",
|
||||
"./dist/i18n/zh-TW": "./dist/i18n/zh-TW.js",
|
||||
"./i18n/*": "./dist/i18n/*",
|
||||
"./lite/*": {
|
||||
"types": "./dist/src/main.d.ts",
|
||||
"require": "./dist/ArtalkLite.js",
|
||||
"default": "./dist/ArtalkLite.es.js"
|
||||
},
|
||||
"./style": "./dist/Artalk.css"
|
||||
},
|
||||
"main": "./dist/Artalk.js",
|
||||
"unpkg": "./dist/Artalk.iife.js",
|
||||
"jsdelivr": "./dist/Artalk.iife.js",
|
||||
"unpkg": "./dist/Artalk.iife.js",
|
||||
"module": "./dist/Artalk.es.js",
|
||||
"types": "./dist/src/main.d.ts",
|
||||
"typings": "./dist/src/main.d.ts",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build && pnpm build:lite && pnpm build:i18n",
|
||||
"build:lite": "vite build --config vite-lite.config.ts",
|
||||
"build:i18n": "node scripts/build-i18n.mjs",
|
||||
"build:demo": "vite build --config vite-demo.config.ts",
|
||||
"serve": "vite preview",
|
||||
"deploy": "gh-pages -d deploy",
|
||||
"predeploy": "pnpm build && pnpm build:demo && pnpm copy:deploy",
|
||||
"build:i18n": "node scripts/build-i18n.mjs",
|
||||
"build:lite": "vite build --config vite-lite.config.ts",
|
||||
"copy:deploy": "cp -r dist deploy && cp CNAME deploy",
|
||||
"lint": "eslint --ext .js,.ts src",
|
||||
"predeploy": "pnpm build && pnpm build:demo && pnpm copy:deploy",
|
||||
"deploy": "gh-pages -d deploy",
|
||||
"dev": "vite",
|
||||
"lint": "eslint --ext .js,.ts src --fix",
|
||||
"serve": "vite preview",
|
||||
"test:e2e": "playwright test",
|
||||
"test:report": "playwright show-report"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/marked": "5.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"abortcontroller-polyfill": "^1.7.5",
|
||||
"hanabi": "^0.4.0",
|
||||
"insane": "^2.6.2",
|
||||
"marked": "^7.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/marked": "5.0.2"
|
||||
}
|
||||
}
|
||||
@@ -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 @@
|
||||
module.exports = {
|
||||
plugins: [
|
||||
require('autoprefixer')
|
||||
]
|
||||
import autoprefixer from 'autoprefixer'
|
||||
|
||||
export default {
|
||||
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,31 @@ 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)
|
||||
}
|
||||
|
||||
/**
|
||||
* Carry user info to request params
|
||||
*
|
||||
* @param params Request params
|
||||
* @returns Request params with user info
|
||||
*/
|
||||
protected withUserInfo(params: any) {
|
||||
const user = this.options.userInfo
|
||||
if (user?.name) params.name = user.name
|
||||
if (user?.email) params.email = user.email
|
||||
return params
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,11 @@ export interface ApiOptions {
|
||||
pageKey: string
|
||||
pageTitle: string
|
||||
timeout?: number
|
||||
apiToken?: string
|
||||
userInfo?: {
|
||||
name: string
|
||||
email: string
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// Hooks
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
import 'abortcontroller-polyfill/dist/polyfill-patch-fetch'
|
||||
|
||||
import { ApiOptions } from './_options'
|
||||
import User from '../lib/user'
|
||||
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 (User.data.token) {
|
||||
if (opts.apiToken) {
|
||||
const headers = new Headers(init.headers) // 保留原有 headers
|
||||
headers.set('Authorization', `Bearer ${User.data.token}`)
|
||||
headers.set('Authorization', `Bearer ${opts.apiToken}`)
|
||||
init.headers = headers
|
||||
}
|
||||
|
||||
@@ -32,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)
|
||||
@@ -62,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',
|
||||
}
|
||||
@@ -70,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) */
|
||||
@@ -82,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
|
||||
}
|
||||
|
||||
@@ -95,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
|
||||
}
|
||||
@@ -1,14 +1,18 @@
|
||||
import type { CommentData, ListData } from '~/types'
|
||||
import * as Utils from '../lib/utils'
|
||||
import ApiBase from './_base'
|
||||
import User from '../lib/user'
|
||||
|
||||
/**
|
||||
* 评论 API
|
||||
*/
|
||||
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,
|
||||
@@ -17,10 +21,8 @@ export default class CommentApi extends ApiBase {
|
||||
}
|
||||
|
||||
if (flatMode) params.flat_mode = flatMode // 平铺模式
|
||||
if (User.checkHasBasicUserInfo()) {
|
||||
params.name = User.data.nick
|
||||
params.email = User.data.email
|
||||
}
|
||||
|
||||
this.withUserInfo(params)
|
||||
|
||||
if (paramsEditor) paramsEditor(params)
|
||||
|
||||
@@ -28,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,
|
||||
@@ -43,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
|
||||
}
|
||||
|
||||
/** 评论 · 修改 */
|
||||
@@ -53,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,12 +84,9 @@ export default class CommentApi extends ApiBase {
|
||||
type,
|
||||
}
|
||||
|
||||
if (User.checkHasBasicUserInfo()) {
|
||||
params.name = User.data.nick
|
||||
params.email = User.data.email
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import { ToFormData } from './_request'
|
||||
import ApiBase from './_base'
|
||||
import User from '../lib/user'
|
||||
|
||||
/**
|
||||
* 上传 API
|
||||
@@ -9,20 +8,20 @@ export default class UploadApi extends ApiBase {
|
||||
/** 图片上传 */
|
||||
public async imgUpload(file: File) {
|
||||
const params: any = {
|
||||
name: User.data.nick,
|
||||
email: User.data.email,
|
||||
page_key: this.options.pageKey,
|
||||
}
|
||||
|
||||
this.withUserInfo(params)
|
||||
|
||||
const form = ToFormData(params)
|
||||
form.set('file', file)
|
||||
|
||||
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 }
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { UserData, NotifyData, UserDataForAdmin } from '~/types'
|
||||
import ApiBase from './_base'
|
||||
import { ToFormData } from './_request'
|
||||
import User from '../lib/user'
|
||||
|
||||
/**
|
||||
* 用户 API
|
||||
@@ -10,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`, {
|
||||
@@ -29,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,
|
||||
@@ -37,17 +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', {
|
||||
name: User.data.nick,
|
||||
email: User.data.email
|
||||
})
|
||||
return (data || { is_login: false, is_admin: false }) as { is_login: boolean, is_admin: boolean }
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
/** 用户 · 注销 */
|
||||
@@ -66,15 +70,18 @@ export default class UserApi extends ApiBase {
|
||||
delete params.comment_id
|
||||
delete params.notify_key
|
||||
params.read_all = true
|
||||
params.name = User.data.nick
|
||||
params.email = User.data.email
|
||||
this.withUserInfo(params)
|
||||
}
|
||||
|
||||
return this.POST(`/mark-read`, params)
|
||||
}
|
||||
|
||||
/** 用户 · 列表 */
|
||||
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,
|
||||
@@ -83,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 }
|
||||
}
|
||||
|
||||
/** 用户 · 新增 */
|
||||
@@ -101,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
|
||||
}
|
||||
|
||||
/** 用户 · 修改 */
|
||||
@@ -120,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,17 +1,22 @@
|
||||
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 ConcreteContext from './context'
|
||||
import defaults from './defaults'
|
||||
import Context from './context'
|
||||
import { handelCustomConf, convertApiOptions } from './config'
|
||||
import Services from './service'
|
||||
import { DefaultPlugins } from './plugins'
|
||||
import * as Stat from './plugins/stat'
|
||||
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
|
||||
@@ -32,16 +37,16 @@ export default class Artalk {
|
||||
if (this.conf.el instanceof HTMLElement) this.$root = this.conf.el
|
||||
|
||||
// Init Context
|
||||
this.ctx = new ConcreteContext(this.conf, this.$root)
|
||||
this.ctx = new Context(this.conf, this.$root)
|
||||
|
||||
// Init Services
|
||||
Object.entries(Services).forEach(([name, initService]) => {
|
||||
const obj = initService(this.ctx)
|
||||
if (obj) this.ctx.inject(name as any, obj) // auto inject deps to ctx
|
||||
if (obj) this.ctx.inject(name as keyof TInjectedServices, obj) // auto inject deps to ctx
|
||||
})
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
@@ -90,9 +104,6 @@ export default class Artalk {
|
||||
// Static Members
|
||||
// ===========================
|
||||
|
||||
/** Default Configs */
|
||||
public static readonly defaults: ArtalkConfig = defaults
|
||||
|
||||
/** Init Artalk */
|
||||
public static init(conf: Partial<ArtalkConfig>): Artalk {
|
||||
return new Artalk(conf)
|
||||
@@ -113,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
|
||||
|
||||
Loaded 100 of 227 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user