Compare commits

..
22 Commits
Author SHA1 Message Date
Mr.Hope ba8a221833 style: fix eslint 2023-12-19 21:05:54 +08:00
Mr.Hope 3f73cd9443 style: introduce prettier 2023-12-19 20:32:04 +08:00
qwqcode 7a0a9dbd92 chore(ci): fix ci pnpm version 2023-12-19 17:07:23 +08:00
dependabot[bot] 441c4fa6a9 chore(deps/ui): bump the dev-deps group in /ui with 3 updates (#698)
Bumps the dev-deps group in /ui with 3 updates: [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin), [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) and [@vue/tsconfig](https://github.com/vuejs/tsconfig).


Updates `@typescript-eslint/eslint-plugin` from 6.14.0 to 6.15.0
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v6.15.0/packages/eslint-plugin)

Updates `@typescript-eslint/parser` from 6.14.0 to 6.15.0
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v6.15.0/packages/parser)

Updates `@vue/tsconfig` from 0.5.0 to 0.5.1
- [Commits](https://github.com/vuejs/tsconfig/compare/v0.5.0...v0.5.1)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/eslint-plugin"
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-deps
- dependency-name: "@typescript-eslint/parser"
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-deps
- dependency-name: "@vue/tsconfig"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-deps
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-19 17:04:01 +08:00
Mr.Hope a16169f69d chore(docs): fix redocly config (#696) 2023-12-19 17:00:58 +08:00
da42676656 chore(deps): bump frontend deps (#679)
* chore: bump deps

* chore: add exports field

* chore: remove src folder

* fix: vite cjs deprecated warning

* chore(ci): fix test ci

* chore: update linter

---------

Co-authored-by: qwqcode <qwqcode@gmail.com>
Co-authored-by: qwqcode <22412567+qwqcode@users.noreply.github.com>
2023-12-19 16:54:34 +08:00
qwqcode f3a42e892f chore(ci): fix test ci 2023-12-18 23:59:18 +08:00
Mr.Hope 6a53597485 chore(docs): update README.md (#691) 2023-12-18 23:19:46 +08:00
qwqcode 60336e567d test(ci): migrate UI tests back to Github Actions (#692) 2023-12-18 23:16:16 +08:00
Mr.Hopeandqwqcode 8918c62ffd chore: update description and fix typos (#684)
* chore: update description

* chore: fix typos

* chore: tweaks

* chore: fix case

---------

Co-authored-by: qwqcode <qwqcode@gmail.com>
2023-12-18 21:21:42 +08:00
qwqcode 362fc19cd8 feat: patch locale config from zh-cn to zh-CN (#678) 2023-12-18 20:17:04 +08:00
qwqcode 1b938752ff fix(ui/dark-mode): potential memory leak issue in auto mode (#688) 2023-12-18 19:14:58 +08:00
qwqcode 835a886dc8 test(ssg): add SSG test case (#675) 2023-12-18 19:09:04 +08:00
qwqcode f390b57fe7 refactor(ui): move window references into function scope (#675) 2023-12-18 19:05:57 +08:00
qwqcode 6d21a0f204 refactor(ui/api): losing coupling between Api and User 2023-12-18 18:37:18 +08:00
qwqcode a1d0c37828 refactor(ui/user): user class no longer globally singleton 2023-12-18 18:37:18 +08:00
github-actions[bot] 763fbc417e chore: release v2.7.1 2023-12-17 17:24:29 +00:00
qwqcode 776d5e66fe docs(i18n): add translation of update notice 2023-12-18 01:22:53 +08:00
qwqcode 10c0975ce0 fix(build): generate source maps to ease debugging 2023-12-18 00:53:54 +08:00
qwqcode 7286642fc9 fix(ui): static method export issue in JS module (#669) 2023-12-18 00:46:56 +08:00
qwqcode 8db4fb3524 fix(cout-widget): loadCountWidget issue 2023-12-17 23:50:44 +08:00
qwqcode 08be04979d chore(docs): bump Artalk to v2.7.0 2023-12-17 20:33:21 +08:00
227 changed files with 6664 additions and 3071 deletions

No files matched your search

+1 -31
View File
@@ -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:
-1
View File
@@ -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:
+47
View File
@@ -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
+13
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -4,12 +4,12 @@
# Artalk
[![](https://img.shields.io/npm/v/artalk.svg?style=flat-square)](https://www.npmjs.com/package/artalk)
[![](https://img.shields.io/npm/dt/artalk.svg?style=flat-square)](https://www.npmjs.com/package/artalk)
[![](https://badgen.net/bundlephobia/minzip/artalk?style=flat-square)](https://bundlephobia.com/package/artalk)
[![npm 版本](https://img.shields.io/npm/v/artalk.svg?style=flat-square)](https://www.npmjs.com/package/artalk)
[![npm 下载量](https://img.shields.io/npm/dt/artalk.svg?style=flat-square)](https://www.npmjs.com/package/artalk)
[![尺寸](https://badgen.net/bundlephobia/minzip/artalk?style=flat-square)](https://bundlephobia.com/package/artalk)
[![CircleCI](https://circleci.com/gh/ArtalkJS/Artalk/tree/master.svg?style=svg)](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)
+3 -3
View File
@@ -4,8 +4,8 @@
# Artalk
[![](https://img.shields.io/npm/v/artalk.svg?style=flat-square)](https://www.npmjs.com/package/artalk)
[![](https://img.shields.io/npm/dt/artalk.svg?style=flat-square)](https://www.npmjs.com/package/artalk)
[![npm version](https://img.shields.io/npm/v/artalk.svg?style=flat-square)](https://www.npmjs.com/package/artalk)
[![npm downloads](https://img.shields.io/npm/dt/artalk.svg?style=flat-square)](https://www.npmjs.com/package/artalk)
[![Docker Pulls](https://img.shields.io/docker/pulls/artalk/artalk-go?style=flat-square)](https://hub.docker.com/r/artalk/artalk-go)
[![Go Report Card](https://goreportcard.com/badge/github.com/ArtalkJS/Artalk?style=flat-square)](https://goreportcard.com/report/github.com/ArtalkJS/Artalk)
[![CircleCI](https://img.shields.io/circleci/build/gh/ArtalkJS/Artalk?style=flat-square)](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
View File
@@ -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 -1
View File
@@ -1,3 +1,3 @@
{
"latest": "2.7.0"
"latest": "2.7.1"
}
+1 -1
View File
@@ -12,7 +12,7 @@ npm install artalk
## 创建实例 `init`
`Artalk.init` 函数和 `new Artalk` 等价,调用该函数将创建并得到一个 Artalk 的实例化对象,可用于后续操作。
调用该函数将创建并得到一个 Artalk 的实例化对象,可用于后续操作。
```js
import Artalk from 'artalk'
+2 -3
View File
@@ -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
View File
@@ -6,7 +6,7 @@ titleTemplate: 自托管评论系统
hero:
name: Artalk
text: The Selfhosted Comment System
text: A Self-hosted Comment System
tagline: 轻量、安全、易上手的自托管评论系统
actions:
- theme: brand
+2 -2
View File
@@ -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
View File
@@ -25,6 +25,6 @@
"vitepress": "1.0.0-rc.14"
},
"dependencies": {
"artalk": "^2.6.4"
"artalk": "^2.7.0"
}
}
+4 -4
View File
@@ -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
View File
@@ -4,9 +4,9 @@ extends:
- recommended
apis:
artalk@v1:
artalk@v2:
root: ./swagger/swagger.json
rules: []
rules: {}
theme:
openapi:
+15
View File
@@ -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属地功能配置修补
+31
View File
@@ -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")
})
}
+2 -2
View File
@@ -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
+4
View File
@@ -0,0 +1,4 @@
/.vitepress/dist/
/.vitepress/.temp/
/.vitepress/cache/
/public/http-api.html
+28
View File
@@ -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' }
]
}
})
+80
View File
@@ -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>
+12
View File
@@ -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>
+19
View File
@@ -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>
+19
View File
@@ -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
+139
View File
@@ -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;
}
+49
View File
@@ -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).
+25
View File
@@ -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
---
+85
View File
@@ -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).
+22
View File
@@ -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"
}
}
+1249
View File
File diff suppressed because it is too large. Load diff
-4
View File
@@ -9,10 +9,6 @@ const route = useRoute()
let artalk: Artalk
Artalk.use((ctx) => {
ctx.fetch
})
onMounted(() => {
artalk = Artalk.init({
el: el.value,
-14
View File
@@ -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
View File
@@ -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'],
},
}
-57
View File
@@ -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
+11
View File
@@ -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
-6
View File
@@ -1,6 +0,0 @@
{
"semi": false,
"arrowParens": "always",
"singleQuote": true,
"htmlWhitespaceSensitivity": "ignore"
}
+29 -22
View File
@@ -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
View File
@@ -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
View File
@@ -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 -1
View File
@@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
+13 -13
View File
@@ -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()
+24 -13
View File
@@ -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">
+13 -12
View File
@@ -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,
}
+26 -16
View File
@@ -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 -2
View File
@@ -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')
})
+67 -34
View File
@@ -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 {
+48 -23
View File
@@ -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;
+24 -18
View File
@@ -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>
+51 -21
View File
@@ -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 {
+33 -13
View File
@@ -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,
}
})
+13 -8
View File
@@ -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
View File
@@ -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>
+11 -8
View File
@@ -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";`,
},
},
},
})
-7
View File
@@ -1,7 +0,0 @@
/*
!dist/
!src/
!types/
!package.json
!README.md
!LICENSE
+1 -1
View File
@@ -4,7 +4,7 @@
# Artalk
> The Selfhosted Comment System
> A Self-hosted Comment System
## Links
+283 -141
View File
@@ -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>
+43 -17
View File
@@ -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"
}
}
+13 -10
View File
@@ -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,
},
],
})
+4 -4
View File
@@ -1,5 +1,5 @@
module.exports = {
plugins: [
require('autoprefixer')
]
import autoprefixer from 'autoprefixer'
export default {
plugins: [autoprefixer],
}
+4 -3
View File
@@ -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`,
})
})
+22 -7
View File
@@ -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
}
}
+5
View File
@@ -4,6 +4,11 @@ export interface ApiOptions {
pageKey: string
pageTitle: string
timeout?: number
apiToken?: string
userInfo?: {
name: string
email: string
}
// -------------------------------------------------------------------
// Hooks
+51 -25
View File
@@ -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
}
+22 -14
View File
@@ -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 -4
View File
@@ -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)
})
+21 -10
View File
@@ -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
}
}
+7 -6
View File
@@ -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)
}
}
+12 -3
View File
@@ -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
}
}
}
+4 -5
View File
@@ -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 }
}
}
+25 -18
View File
@@ -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),
})
}
}
+26 -15
View File
@@ -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,
})
}
}
+46 -26
View File
@@ -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)
+7 -4
View File
@@ -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}`,
}
}
+16 -12
View File
@@ -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