Compare commits

...
48 Commits
Author SHA1 Message Date
qwqcode c99caa035f v2.1.7
Signed-off-by: qwqcode <qwqcode@gmail.com>
2022-04-13 00:26:02 +08:00
qwqcode 2fc24ef753 feat(pin): Comment show pinned badge.
Signed-off-by: qwqcode <qwqcode@gmail.com>
2022-04-13 00:23:20 +08:00
qwqcode c6d19a3e40 feat(pin): Admin edit comment is_pinned field.
Signed-off-by: qwqcode <qwqcode@gmail.com>
2022-04-12 22:49:26 +08:00
qwqcode 154eaad8f1 feat(list): Dropdown select SortBy & ViewOnlyAdmin.
Signed-off-by: qwqcode <qwqcode@gmail.com>
2022-04-12 22:24:25 +08:00
qwqcode 7137a5c834 v2.1.6 2022-04-12 17:00:36 +08:00
qwqcode 93c2d88dbc fix(check-admin-show-el): Sidebar not included.
Signed-off-by: qwqcode <qwqcode@gmail.com>
2022-04-12 16:41:37 +08:00
qwqcode edd9a27720 fix(captcha): Image size too small to be recognized.
Signed-off-by: qwqcode <qwqcode@gmail.com>
2022-04-12 16:21:41 +08:00
qwqcode fa5703d6e0 feat(pv): Page PV counter.
Signed-off-by: qwqcode <qwqcode@gmail.com>
2022-04-12 14:03:04 +08:00
qwqcode 8dd40a0c9f chore: Remove sidebar layer checkReqStatus temporarily.
Signed-off-by: qwqcode <qwqcode@gmail.com>
2022-04-12 12:13:50 +08:00
qwqcode f779e0487d fix: Set z-index of SidebarLayer header.
Signed-off-by: qwqcode <qwqcode@gmail.com>
2022-04-12 00:56:18 +08:00
qwqcode 82d3563e62 v2.1.5 2022-04-11 23:06:03 +08:00
qwqcode a1579d71c2 chore: Add build:all script
Signed-off-by: qwqcode <qwqcode@gmail.com>
2022-04-11 22:37:31 +08:00
qwqcode 8d6170d645 Merge pull request #92 from ArtalkJS/sidebar-sep
🎉 从前端代码中分离侧边栏,并置入到后端中
2022-04-11 22:34:32 +08:00
qwqcode 9e345f2b4b chore(deps): Upgrade marked to 4.0.14
Signed-off-by: qwqcode <qwqcode@gmail.com>
2022-04-11 22:06:02 +08:00
qwqcode f00ffac57a fix(UA): Client-side distinguish between Windows 10 and 11
Signed-off-by: qwqcode <qwqcode@gmail.com>
2022-04-11 21:54:32 +08:00
qwqcode 7370986186 feat(sidebar): Remove artransfer type select
Signed-off-by: qwqcode <qwqcode@gmail.com>
2022-04-11 20:42:16 +08:00
qwqcode 39b32b03d2 feat(sidebar): Test iframe URL accessibility & error alert
Signed-off-by: qwqcode <qwqcode@gmail.com>
2022-04-11 20:21:15 +08:00
qwqcode fba2d99ae2 refactor(sidebar): Sidebar iframe loading animation.
Signed-off-by: qwqcode <qwqcode@gmail.com>
2022-04-11 19:33:29 +08:00
qwqcode e586c55478 refactor(sidebar): Dark mode support
Signed-off-by: qwqcode <qwqcode@gmail.com>
2022-04-11 19:23:52 +08:00
qwqcode 16d1a82aff fix(sidebar): User override mode & sidebar iframe refresh.
Signed-off-by: qwqcode <qwqcode@gmail.com>
2022-04-11 17:28:50 +08:00
qwqcode 0c47986d56 chore: Modify development check method
Signed-off-by: qwqcode <qwqcode@gmail.com>
2022-04-11 17:16:54 +08:00
qwqcode 44dabbf05f fix(api): Need-Password dialog while 400 http code.
Signed-off-by: qwqcode <qwqcode@gmail.com>
2022-04-11 17:13:32 +08:00
qwqcode 5c43e8c197 refactor(sidebar): Iframe pass user data
Signed-off-by: qwqcode <qwqcode@gmail.com>
2022-04-11 16:27:17 +08:00
qwqcode 1b99bcd4fa refactor: Further increase the independence of the sidebar.
Signed-off-by: qwqcode <qwqcode@gmail.com>
2022-04-11 15:53:48 +08:00
qwqcode 6e90f4677a fix(eslint): Monorepo import/resolver config 2022-04-11 15:46:09 +08:00
qwqcode 3253e6d45c refactor: re-import and separate sidebar basically.
Signed-off-by: qwqcode <qwqcode@gmail.com>
2022-04-11 14:36:44 +08:00
qwqcode 0e49c38853 feat: Init ArtalkSidebar package.
Signed-off-by: qwqcode <qwqcode@gmail.com>
2022-04-11 13:15:24 +08:00
qwqcode c906384e53 fix(types): Wrong heightLimit type requirements. 2022-04-11 13:10:16 +08:00
qwqcode 1e3fbac132 fix: .eslintrc configuration
Signed-off-by: qwqcode <qwqcode@gmail.com>
2022-04-11 11:44:51 +08:00
qwqcode e54b0e8e63 Merge pull request #89 from ArtalkJS/monorepo
refactor: re-organize into monorepo
2022-04-11 11:41:21 +08:00
qwqcode eeade17262 refactor: re-organize into monorepo
Signed-off-by: qwqcode <qwqcode@gmail.com>
2022-04-11 11:40:40 +08:00
qwqcode 79e51aea4e Merge pull request #88 from kecrily/fix/windows11-ua
fix(UA): distinguish between Windows 10/11
2022-04-10 19:34:01 +08:00
Percy Ma a4e8b06010 fix(UA): distinguish between Windows 10/11
https://docs.microsoft.com/en-us/microsoft-edge/web-platform/how-to-detect-win11
close #78
2022-04-10 19:30:42 +08:00
qwqcode 558289e904 Merge pull request #87 from ArtalkJS/dependabot/npm_and_yarn/nanoid-3.3.2
chore(deps): bump nanoid from 3.1.30 to 3.3.2
2022-04-10 01:59:23 +08:00
dependabot[bot] 14fba98000 chore(deps): bump nanoid from 3.1.30 to 3.3.2
Bumps [nanoid](https://github.com/ai/nanoid) from 3.1.30 to 3.3.2.
- [Release notes](https://github.com/ai/nanoid/releases)
- [Changelog](https://github.com/ai/nanoid/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ai/nanoid/compare/3.1.30...3.3.2)

---
updated-dependencies:
- dependency-name: nanoid
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-04-09 17:51:55 +00:00
qwqcode 5538de0258 Merge pull request #85 from ArtalkJS/dependabot/npm_and_yarn/minimist-1.2.6
chore(deps): bump minimist from 1.2.5 to 1.2.6
2022-04-10 01:51:30 +08:00
dependabot[bot] 0fbababecd chore(deps): bump minimist from 1.2.5 to 1.2.6
Bumps [minimist](https://github.com/substack/minimist) from 1.2.5 to 1.2.6.
- [Release notes](https://github.com/substack/minimist/releases)
- [Commits](https://github.com/substack/minimist/compare/1.2.5...1.2.6)

---
updated-dependencies:
- dependency-name: minimist
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-24 04:15:08 +00:00
qwqcode 9193ad4f98 v2.1.4 2021-11-22 23:02:34 +08:00
qwqcode db409dea1a refactor: ListLite.
Signed-off-by: qwqcode <qwqcode@gmail.com>
2021-11-22 17:05:10 +08:00
qwqcode 7347549904 chore: Use eslint-airbnb-base.
Signed-off-by: qwqcode <qwqcode@gmail.com>
2021-11-22 13:57:14 +08:00
qwqcode 6e6e6549dd refactor: Modify css color variable reference method.
Signed-off-by: qwqcode <qwqcode@gmail.com>
2021-11-21 19:42:55 +08:00
qwqcode 217ec7bbf4 refactor(Comment): Simplify html and css structure.
Signed-off-by: qwqcode <qwqcode@gmail.com>
2021-11-21 19:23:00 +08:00
qwqcode d09fa0fb6f refactor(Editor): Simplify html and css structure.
Signed-off-by: qwqcode <qwqcode@gmail.com>
2021-11-21 17:12:33 +08:00
qwqcode fef01ccebd fix(UnreadBadge): Admin user cannot display unread badge.
Signed-off-by: qwqcode <qwqcode@gmail.com>
2021-11-21 14:09:51 +08:00
qwqcode 634b31b7d0 refactor: ListLite x ReadMore btn.
Signed-off-by: qwqcode <qwqcode@gmail.com>
2021-11-21 13:45:16 +08:00
qwqcode aeaed307b4 feat(emoticons): Unified image emoticon width.
Signed-off-by: qwqcode <qwqcode@gmail.com>
2021-11-21 12:25:19 +08:00
qwqcode 1392efca59 refactor: Comment ListLite part & Height limit.
Signed-off-by: qwqcode <qwqcode@gmail.com>
2021-11-21 01:21:47 +08:00
qwqcode 5099ae5053 fix(Pagination): FlatMore x Pagination comments cannot load totally.
Signed-off-by: qwqcode <qwqcode@gmail.com>
2021-11-20 13:05:52 +08:00
96 changed files with 2385 additions and 2302 deletions

No files matched your search

+3
View File
@@ -6,3 +6,6 @@ dist
coverage
vite*.config*
# 不检测 js
**/*.js
+16 -12
View File
@@ -1,34 +1,38 @@
module.exports = {
root: true,
parser: '@typescript-eslint/parser',
parser: "@typescript-eslint/parser",
parserOptions: {
project: ['./packages/*/tsconfig.json'],
tsconfigRootDir: __dirname,
},
extends: [
'airbnb',
'airbnb-typescript',
'airbnb-base',
'airbnb-typescript/base',
'prettier',
'plugin:import/recommended',
'plugin:import/typescript',
],
plugins: ['@typescript-eslint', 'import'],
rules: {
'@typescript-eslint/lines-between-class-members': 0,
'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': 'off',
'no-plusplus': 0,
'no-param-reassign': 0,
'no-console': 0,
'react/sort-comp': 0,
'@typescript-eslint/no-use-before-define': 0,
'no-constructor-return': 0,
'class-methods-use-this': 0,
'import/no-cycle': 0,
'spaced-comment': 0,
'no-lonely-if': 0,
'prefer-destructuring': 0,
'import/no-cycle': 0,
'@typescript-eslint/lines-between-class-members': 0,
'@typescript-eslint/no-unused-vars': 'off',
'@typescript-eslint/no-use-before-define': 0,
},
settings: {
'import/resolver': {
typescript: {},
typescript: {
"project": [__dirname + "/packages/*/tsconfig.json"]
},
},
},
parserOptions: {
project: './tsconfig.json',
},
}
+20 -7
View File
@@ -8,19 +8,32 @@ on:
jobs:
publish:
# prevents this action from running on forks
if: github.repository == 'ArtalkJS/Artalk'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
# Setup .npmrc file to publish to npm
- uses: actions/setup-node@v2
- name: Checkout
uses: actions/checkout@v3
- name: Set node version to 12.x
uses: actions/setup-node@v2
with:
node-version: '12.x'
registry-url: 'https://registry.npmjs.org'
- run: npm install
- run: npm run build
- run: npm publish
- name: Install dependencies
run: yarn install
- name: Build
run: yarn build
- name: Publish
run: cd packages/artalk && yarn publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
# GitHub Packages
#
# Setup .npmrc file to publish to GitHub Packages
# - uses: actions/setup-node@v2
# with:
@@ -29,6 +42,7 @@ jobs:
# - run: npm publish
# env:
# NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Docs Update
docs-update:
runs-on: ubuntu-latest
@@ -51,4 +65,3 @@ jobs:
- uses: stefanzweifel/git-auto-commit-action@v4
with:
commit_message: Update Artalk CDN link to v${{ steps.get_version.outputs.VERSION }}
+7 -3
View File
@@ -12,19 +12,23 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Setup
uses: actions/setup-node@v1
with:
node-version: '12.x'
- name: Install
run: yarn install
- name: Build
run: yarn predeploy
run: cd packages/artalk && yarn predeploy
- name: Deploy
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_branch: gh-pages
publish_dir: ./deploy
publish_dir: ./packages/artalk/deploy
user_name: 'github-actions[bot]'
user_email: 'github-actions[bot]@users.noreply.github.com'
user_email: 'github-actions[bot]@users.noreply.github.com'
+14 -48
View File
@@ -1,67 +1,33 @@
{
"name": "artalk",
"version": "2.1.3",
"description": "A Selfhosted Comment System",
"keywords": [
"comment-system",
"comments-widget",
"comment",
"comments",
"blog"
"name": "artalk-monorepo",
"private": true,
"workspaces": [
"packages/*"
],
"homepage": "https://artalk.js.org",
"bugs": "https://github.com/ArtalkJS/Artalk/issue",
"license": "GPL-3.0",
"repository": {
"type": "git",
"url": "https://github.com/ArtalkJS/Artalk.git"
},
"main": "./dist/Artalk.js",
"unpkg": "./dist/Artalk.iife.js",
"jsdelivr": "./dist/Artalk.iife.js",
"module": "./dist/Artalk.es.js",
"types": "./types/index.d.ts",
"exports": {
".": {
"import": "./dist/Artalk.es.js",
"require": "./dist/Artalk.js"
}
},
"scripts": {
"dev": "vite",
"build": "yarn lint && vite build",
"build:demo": "vite build --config vite-demo.config.ts",
"serve": "vite preview",
"deploy": "gh-pages -d deploy",
"predeploy": "yarn run build && yarn run build:demo && yarn run copy:deploy",
"copy:deploy": "cp -r dist deploy && cp CNAME deploy",
"lint": "eslint --ext .js,.ts src"
"build": "yarn --cwd packages/artalk run build",
"dev": "yarn --cwd packages/artalk run dev",
"build:sidebar": "yarn run build && yarn --cwd packages/artalk-sidebar run build",
"build:all": "yarn --cwd packages/artalk run build && yarn --cwd packages/artalk-sidebar run build"
},
"devDependencies": {
"@rollup/plugin-dynamic-import-vars": "^1.4.1",
"@types/marked": "^4.0.1",
"@types/node": "^16.11.7",
"@typescript-eslint/eslint-plugin": "^5.4.0",
"@typescript-eslint/parser": "^5.4.0",
"autoprefixer": "^10.4.0",
"eslint": "7.2.0",
"eslint-config-airbnb": "18.2.1",
"eslint-config-airbnb-typescript": "^14.0.0",
"eslint": "^8.2.0",
"eslint-config-airbnb-base": "15.0.0",
"eslint-config-airbnb-typescript": "^16.0.0",
"eslint-config-prettier": "^8.3.0",
"eslint-import-resolver-typescript": "^2.4.0",
"eslint-plugin-import": "2.22.1",
"eslint-plugin-jsx-a11y": "^6.4.1",
"eslint-plugin-react": "^7.25.1",
"eslint-plugin-react-hooks": "^4.2.0",
"eslint-import-resolver-typescript": "^2.5.0",
"eslint-plugin-import": "^2.25.2",
"less": "^4.1.2",
"prettier": "^2.4.1",
"typescript": "^4.5.2",
"vite": "^2.6.14",
"vite-tsconfig-paths": "^3.3.17"
},
"dependencies": {
"hanabi": "^0.4.0",
"insane": "^2.6.2",
"marked": "^4.0.3"
}
"dependencies": {}
}
+14
View File
@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Artalk Sidebar</title>
</head>
<body>
<div id="ArtalkSidebar"></div>
<script type="module" src="./src/index.ts"></script>
</body>
</html>
+35
View File
@@ -0,0 +1,35 @@
{
"name": "@artalkjs/artalk-sidebar",
"version": "0.1.0",
"license": "GPL-2.0",
"description": "The sidebar of artalk",
"repository": {
"type": "git",
"url": "https://github.com/ArtalkJS/Artalk.git",
"directory": "packages/artalk-sidebar"
},
"homepage": "https://artalk.js.org",
"bugs": "https://github.com/ArtalkJS/Artalk/issue",
"main": "./dist/ArtalkSidebar.js",
"unpkg": "./dist/ArtalkSidebar.iife.js",
"jsdelivr": "./dist/ArtalkSidebar.iife.js",
"module": "./dist/ArtalkSidebar.es.js",
"types": "./types/index.d.ts",
"exports": {
".": {
"import": "./dist/ArtalkSidebar.es.js",
"require": "./dist/ArtalkSidebar.js"
}
},
"scripts": {
"dev": "vite --port 23367",
"build": "yarn lint && vite build",
"serve": "vite preview",
"lint": "eslint --ext .js,.ts src"
},
"devDependencies": {
},
"dependencies": {
"artalk": "*"
}
}
@@ -1,13 +1,13 @@
import '@/style/page-list.less'
import '../style/page-list.less'
import Context from '@/context'
import Component from '@/lib/component'
import * as Utils from '@/lib/utils'
import * as Ui from '@/lib/ui'
import { PageData } from '~/types/artalk-data'
import Context from 'artalk/src/context'
import Component from 'artalk/src/lib/component'
import * as Utils from 'artalk/src/lib/utils'
import * as Ui from 'artalk/src/lib/ui'
import { PageData } from 'artalk/types/artalk-data'
import Api from 'artalk/src/api'
import ActionBtn from 'artalk/src/components/action-btn'
import ItemTextEditor from '../item-text-editor'
import Api from '~/src/api'
import ActionBtn from '../action-btn'
export default class PageList extends Component {
$editor?: HTMLElement
@@ -1,10 +1,10 @@
import '@/style/site-list.less'
import '../style/site-list.less'
import Context from '@/context'
import * as Utils from '@/lib/utils'
import * as Ui from '@/lib/ui'
import { SiteData } from '~/types/artalk-data'
import Api from '~/src/api'
import Context from 'artalk/src/context'
import * as Utils from 'artalk/src/lib/utils'
import * as Ui from 'artalk/src/lib/ui'
import { SiteData } from 'artalk/types/artalk-data'
import Api from 'artalk/src/api'
interface SiteListFloaterConf {
onSwitchSite: (siteName: string) => boolean|void
@@ -1,12 +1,12 @@
import '@/style/site-list.less'
import '../style/site-list.less'
import Context from '@/context'
import Component from '@/lib/component'
import * as Utils from '@/lib/utils'
import * as Ui from '@/lib/ui'
import { SiteData } from '~/types/artalk-data'
import Context from 'artalk/src/context'
import Component from 'artalk/src/lib/component'
import * as Utils from 'artalk/src/lib/utils'
import * as Ui from 'artalk/src/lib/ui'
import { SiteData } from 'artalk/types/artalk-data'
import Api from 'artalk/src/api'
import ItemTextEditor from '../item-text-editor'
import Api from '~/src/api'
export default class SiteList extends Component {
sites: SiteData[] = []
@@ -0,0 +1,31 @@
import Artalk from 'artalk'
import ArtalkConfig, { LocalUser } from 'artalk/types/artalk-config'
import 'artalk/dist/Artalk.css'
import Sidebar from './sidebar'
class ArtalkSidebar extends Artalk {
constructor(customConf: ArtalkConfig, user: LocalUser) {
super(customConf)
this.$root.style.display = 'none'
user = user || {}
this.ctx.user.data = {
...this.ctx.user.data,
...user
}
this.ctx.user.save()
const sidebar = new Sidebar(this.ctx)
document.body.appendChild(sidebar.$el)
if (customConf.darkMode) {
sidebar.$el.classList.add('atk-dark-mode')
}
sidebar.show()
console.log('hello artalk-sidebar')
}
}
export default ArtalkSidebar
+15
View File
@@ -0,0 +1,15 @@
import ArtalkSidebar from './artalk-sidebar'
const searchParams = new URLSearchParams(document.location.search)
const pageKey = searchParams.get('pageKey') || ''
const site = searchParams.get('site') || ''
const user = JSON.parse(searchParams.get('user') || '{}')
const darkMode = searchParams.get('darkMode') === '1'
const artalkSidebar = new ArtalkSidebar({
el: '#ArtalkSidebar',
server: (import.meta.env.MODE === 'development') ? 'http://localhost:23366/api' : '../api',
pageKey,
site,
darkMode,
}, user)
@@ -1,6 +1,6 @@
import '../style/item-text-editor.less'
import './style/item-text-editor.less'
import * as Utils from '../lib/utils'
import * as Utils from 'artalk/src/lib/utils'
interface ItemTextEditorConf {
initValue?: string
File renamed without changes.
@@ -1,7 +1,7 @@
import Context from '../context'
import Component from '../lib/component'
import * as Utils from '../lib/utils'
import Comment from './comment'
import Context from 'artalk/src/context'
import Component from 'artalk/src/lib/component'
import * as Utils from 'artalk/src/lib/utils'
import Comment from 'artalk/src/components/comment'
export default class SidebarView extends Component {
static viewName = ''
@@ -1,7 +1,8 @@
import Context from '../../context'
import Context from 'artalk/src/context'
import * as Utils from 'artalk/src/lib/utils'
import ListLite from 'artalk/src/components/list-lite'
import SidebarView from '../sidebar-view'
import * as Utils from '../../lib/utils'
import ListLite from '../list-lite'
export default class MessageView extends SidebarView {
static viewName = 'comments'
@@ -31,10 +32,10 @@ export default class MessageView extends SidebarView {
this.viewActiveTab = 'mentions'
}
this.list = new ListLite(this.ctx, this.$el)
this.list = new ListLite(this.ctx)
this.list.flatMode = true
this.list.unreadHighlight = true
this.list.autoLoadListenerAt = this.$parent
this.list.scrollListenerAt = this.$parent
this.list.pageMode = 'pagination'
this.list.noCommentText = '<div class="atk-sidebar-no-content">无内容</div>'
this.list.renderComment = (comment) => {
@@ -61,12 +62,11 @@ export default class MessageView extends SidebarView {
switchTab(tab: string, siteName: string): boolean|void {
//console.log(tab, siteName)
this.viewActiveTab = tab
this.list.type = tab as any;
this.list.isFirstLoad = true
this.list.paramsEditor = (params) => {
params.type = tab as any
params.site_name = siteName
}
this.list.reqComments()
this.list.fetchComments(0)
return true
}
@@ -1,11 +1,12 @@
import Api from '~/src/api'
import Context from '../../context'
import Component from '../../lib/component'
import * as Utils from '../../lib/utils'
import * as Ui from '../../lib/ui'
import Api from 'artalk/src/api'
import Context from 'artalk/src/context'
import Component from 'artalk/src/lib/component'
import * as Utils from 'artalk/src/lib/utils'
import * as Ui from 'artalk/src/lib/ui'
import Comment from 'artalk/src/components/comment'
import Pagination, { PaginationConf } from 'artalk/src/components/pagination'
import PageList from '../admin/page-list'
import Comment from '../comment'
import Pagination, { PaginationConf } from '../pagination'
import SidebarView from '../sidebar-view'
const PAGE_SIZE = 20
@@ -1,9 +1,10 @@
import Api from '~/src/api'
import Context from '../../context'
import Component from '../../lib/component'
import * as Utils from '../../lib/utils'
import Api from 'artalk/src/api'
import Context from 'artalk/src/context'
import Component from 'artalk/src/lib/component'
import * as Utils from 'artalk/src/lib/utils'
import Comment from 'artalk/src/components/comment'
import SiteList from '../admin/site-list'
import Comment from '../comment'
import SidebarView from '../sidebar-view'
export default class SettingView extends SidebarView {
@@ -1,9 +1,10 @@
import Api from '~/src/api'
import Context from '../../context'
import Component from '../../lib/component'
import * as Utils from '../../lib/utils'
import Api from 'artalk/src/api'
import Context from 'artalk/src/context'
import Component from 'artalk/src/lib/component'
import * as Utils from 'artalk/src/lib/utils'
import Comment from 'artalk/src/components/comment'
import SiteList from '../admin/site-list'
import Comment from '../comment'
import SidebarView from '../sidebar-view'
export default class SitesView extends SidebarView {
@@ -1,10 +1,11 @@
import Api from '~/src/api'
import Context from '../../context'
import Component from '../../lib/component'
import * as Utils from '../../lib/utils'
import * as Ui from '../../lib/ui'
import Api from 'artalk/src/api'
import Context from 'artalk/src/context'
import Component from 'artalk/src/lib/component'
import * as Utils from 'artalk/src/lib/utils'
import * as Ui from 'artalk/src/lib/ui'
import Comment from 'artalk/src/components/comment'
import SiteList from '../admin/site-list'
import Comment from '../comment'
import SidebarView from '../sidebar-view'
export default class TransferView extends SidebarView {ß
@@ -40,31 +41,20 @@ export default class TransferView extends SidebarView {ß
<div class="atk-log"></div>
</div>
<div class="atk-form">
<div class="atk-label">数据类型</div>
<select name="AtkDataType">
<option value="artrans">Artrans (数据行囊)</option>
<option value="artalk_v1">Artalk v1 (PHP 旧版)</option>
<option value="typecho">Typecho</option>
<option value="wordpress">WordPress</option>
<option value="disqus">Disqus</option>
<option value="commento">Commento</option>
<option value="valine">Valine</option>
<option value="twikoo">Twikoo</option>
</select>
<div class="atk-label atk-data-file-label">数据文件</div>
<input type="file" name="AtkDataFile" accept="text/plain,.json">
<div class="atk-label atk-data-file-label">Artrans 数据文件</div>
<input type="file" name="AtkDataFile" accept="text/plain,.json,.artrans">
<span class="atk-desc">使用「<a href="https://artalk.js.org/guide/transfer.html" target="_blank">转换工具</a>」将评论数据转为 Artrans 格式</span>
<div class="atk-label">目标站点名</div>
<input type="text" name="AtkSiteName" placeholder="输入内容..." autocomplete="off">
<div class="atk-label">目标站点 URL</div>
<input type="text" name="AtkSiteURL" placeholder="输入内容..." autocomplete="off">
<div class="atk-label">启动参数(可选)</div>
<textarea name="AtkPayload"></textarea>
<span class="atk-desc">启动参数查阅:“<a href="https://artalk.js.org/guide/transfer.html" target="_blank">文档 · 数据搬家</a>”</span>
<span class="atk-desc">参考「<a href="https://artalk.js.org/guide/transfer.html" target="_blank">文档 · 数据迁移</a>」</span>
<button class="atk-btn" name="AtkSubmit">导入</button>
</div>`
const $form = this.$el.querySelector<HTMLSelectElement>('.atk-form')!
const $dataType = $form.querySelector<HTMLSelectElement>('[name="AtkDataType"]')!
const $dataFile = $form.querySelector<HTMLInputElement>('[name="AtkDataFile"]')!
const $dataFileLabel = $form.querySelector<HTMLInputElement>('.atk-data-file-label')!
const $siteName = $form.querySelector<HTMLInputElement>('[name="AtkSiteName"]')!
@@ -73,27 +63,12 @@ export default class TransferView extends SidebarView {ß
const $submitBtn = $form.querySelector<HTMLButtonElement>('[name="AtkSubmit"]')!
const setError = (msg: string) => window.alert(msg)
$dataType.onchange = () => {
if (['typecho'].includes($dataType.value)) {
$dataFile.style.display = 'none'
$dataFileLabel.style.display = 'none'
} else {
$dataFile.style.display = ''
$dataFileLabel.style.display = ''
}
}
$submitBtn.onclick = () => {
const dataType = $dataType.value.trim()
const dataType = 'artrans'
const siteName = $siteName.value.trim()
const siteURL = $siteURL.value.trim()
const payload = $payload.value.trim()
if (dataType === '') {
setError('请选择数据类型')
return
}
// 请求 payload 参数制备
let rData: any = {}
if (payload) {
@@ -1,4 +1,4 @@
<div class="atk-sidebar">
<div class="atk-sidebar artalk">
<div class="atk-sidebar-inner">
<div class="atk-sidebar-header">
<span class="atk-avatar">
@@ -7,7 +7,7 @@
<span class="atk-menu">
<span class="atk-item atk-active atk-sidebar-title">控制中心</span>
</span>
<div class="atk-sidebar-close"><i class="atk-icon atk-icon-close"></i></div>
<div class="atk-sidebar-close" style="opacity: 0;"><i class="atk-icon atk-icon-close"></i></div>
</div>
<div class="atk-sidebar-nav">
<div class="akt-curt-view-btn">
@@ -1,32 +1,31 @@
import '../style/sidebar.less'
import './style/sidebar.less'
import MD5 from '../lib/md5'
import Context from '../context'
import Component from '../lib/component'
import * as Utils from '../lib/utils'
import * as Ui from '../lib/ui'
import Comment from './comment'
import SidebarHTML from './html/sidebar.html?raw'
import Layer from './layer'
import Context from 'artalk/src/context'
import Component from 'artalk/src/lib/component'
import * as Utils from 'artalk/src/lib/utils'
import * as Ui from 'artalk/src/lib/ui'
import Comment from 'artalk/src/components/comment'
import { SiteData } from 'artalk/types/artalk-data'
import Api from 'artalk/src/api'
import SidebarHTML from './sidebar.html?raw'
import SidebarView from './sidebar-view'
import CommentsView from './sidebar-views/comments-view'
import PagesView from './sidebar-views/pages-view'
import SitesView from './sidebar-views/sites-view'
import TransferView from './sidebar-views/transfer-view'
import { SiteData } from '~/types/artalk-data'
import Api from '../api'
import SiteListFloater from './admin/site-list-floater'
import SettingView from './sidebar-views/setting-view'
import MD5 from './lib/md5'
const DEFAULT_VIEW = 'comments'
const REGISTER_VIEWS: (typeof SidebarView)[] = [
CommentsView, PagesView, SitesView, TransferView, // SettingView
]
export default class Sidebar extends Component {
public layer?: Layer
public $header: HTMLElement
public $headerMenu: HTMLElement
public $title: HTMLElement
@@ -135,26 +134,6 @@ export default class Sidebar extends Component {
/** 显示 */
public async show() {
this.$el.style.transform = '' // 动画清除,防止二次打开失效
// 获取 Layer
this.layer = new Layer(this.ctx, 'sidebar', this.$el)
this.layer.afterHide = () => {
// 防止评论框被吞
if (this.ctx.conf.editorTravel === true) {
this.ctx.trigger('editor-travel-back')
}
}
this.layer.show()
// viewWrap 滚动条归位
this.$viewWrap.scrollTo(0, 0)
// 执行动画
setTimeout(() => {
this.$el.style.transform = 'translate(0, 0)'
}, 20)
// 第一次加载
if (this.firstShow) {
///////////////////
@@ -218,12 +197,8 @@ export default class Sidebar extends Component {
}
}
/** 隐藏 */
public hide() {
// 执行动画
this.$el.style.transform = ''
this.layer?.hide()
public async hide() {
console.log("hide")
}
/** 切换 View */
@@ -69,7 +69,7 @@
.atk-item {
&:hover {
background: @color-bg-grey;
background: var(--at-color-bg-grey);
}
&:not(:last-child) {
@@ -91,10 +91,10 @@
}
.atk-icon {
background-color: @color-meta;
background-color: var(--at-color-meta);
}
.atk-icon-yes {
background-color: @color-main;
background-color: var(--at-color-main);
}
}
@@ -0,0 +1,4 @@
@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: 4px;
@@ -1,6 +1,6 @@
.atk-item-text-editor-layer {
z-index: 999;
background: @color-bg;
background: var(--at-color-bg);
position: absolute;
left: 0;
top: 0;
@@ -19,7 +19,7 @@
width: 100%;
padding: 3px 5px;
border: 0;
border-bottom: 1px solid @color-border;
border-bottom: 1px solid var(--at-color-border);
outline: none;
background: transparent;
@@ -7,7 +7,7 @@
align-items: center;
&:not(:last-child) {
border-bottom: 1px solid @color-border;
border-bottom: 1px solid var(--at-color-border);
}
}
@@ -18,13 +18,13 @@
padding: 20px 30px;
.atk-title {
color: @color-font;
color: var(--at-color-font);
font-size: 21px;
margin-bottom: 10px;
}
.atk-sub {
color: @color-sub;
color: var(--at-color-sub);
font-size: 14px;
}
}
@@ -39,7 +39,7 @@
.atk-page-edit-layer {
z-index: 999;
background: @color-bg;
background: var(--at-color-bg);
position: absolute;
width: 100%;
height: 100%;
@@ -4,14 +4,12 @@
.atk-sidebar {
@width: 430px;
position: fixed;
z-index: 99999;
z-index: 99998;
top: 0;
right: 0;
width: @width;
height: 100%;
background: @color-bg;
transition: transform 450ms cubic-bezier(0.23, 1, 0.32, 1) 0ms;
transform: translate(@width, 0px);
background: var(--at-color-bg);
@media only screen and (max-width: @width) {
& {
@@ -28,7 +26,7 @@
display: flex;
flex-direction: row;
align-items: center;
border-bottom: 1px solid @color-border;
border-bottom: 1px solid var(--at-color-border);
.atk-avatar {
display: flex;
@@ -42,11 +40,11 @@
height: @height;
width: @header-height - 30px;
border-radius: 2px;
background: @color-meta;
background: var(--at-color-meta);
text-align: center;
line-height: @height;
font-size: 13px;
color: @color-bg;
color: var(--at-color-bg);
}
.atk-site-logo {
@@ -61,7 +59,7 @@
.atk-item {
position: relative;
color: @color-sub;
color: var(--at-color-sub);
font-size: 20px;
// cursor: pointer;
user-select: none;
@@ -71,7 +69,7 @@
}
&.atk-active {
color: @color-font;
color: var(--at-color-font);
&:after {
content: ' ';
@@ -80,7 +78,7 @@
left: 0;
width: 100%;
height: 6px;
background: @color-main;
background: var(--at-color-main);
opacity: .4;
}
}
@@ -108,14 +106,14 @@
.atk-sidebar-nav {
display: flex;
flex-direction: row;
border-bottom: 1px solid @color-border;
border-bottom: 1px solid var(--at-color-border);
height: @nav-height + 1px;
.akt-curt-view-btn {
display: flex;
align-items: center;
padding: 0 20px;
border-right: 1px solid @color-border;
border-right: 1px solid var(--at-color-border);
cursor: pointer;
user-select: none;
height: @nav-height;
@@ -123,7 +121,7 @@
white-space: nowrap;
&:hover {
background: @color-bg-grey;
background: var(--at-color-bg-grey);
}
.atk-icon {
@@ -135,7 +133,7 @@
width: 14px;
height: 1px;
margin: 0 auto 4px;
background: @color-deep;
background: var(--at-color-deep);
transform-origin: 20px;
&:last-child {
@@ -185,18 +183,18 @@
.atk-tab-item {
line-height: @nav-height;
height: @nav-height;
color: @color-sub;
color: var(--at-color-sub);
cursor: pointer;
user-select: none;
padding: 0 20px;
white-space: nowrap;
&:hover {
background: @color-bg-grey;
background: var(--at-color-bg-grey);
}
&.atk-active {
color: @color-deep;
color: var(--at-color-deep);
}
}
}
@@ -211,11 +209,11 @@
top: 70px;
transform: translateX(-50%);
pointer-events: all;
background: @color-bg;
background: var(--at-color-bg);
width: 80%;
max-height: 40%;
margin: 0 auto;
border: 1px solid @color-border;
border: 1px solid var(--at-color-border);
border-radius: 4px;
overflow-y: auto;
@@ -236,22 +234,22 @@
width: 20px;
height: 20px;
line-height: 20px;
background: @color-meta;
background: var(--at-color-meta);
margin: 10px;
border-radius: 3px;
text-align: center;
color: @color-bg;
color: var(--at-color-bg);
font-size: 12px;
}
.atk-site-name {
color: @color-font;
color: var(--at-color-font);
font-size: 17px;
margin-left: 7px;
}
&.atk-active, &:hover {
background: @color-bg-grey;
background: var(--at-color-bg-grey);
}
}
}
@@ -269,7 +267,7 @@
right: 0;
top: 0;
font-size: 35px;
color: @color-meta;
color: var(--at-color-meta);
pointer-events: none;
height: 100%;
display: flex;
@@ -286,16 +284,16 @@
.atk-label {
font-size: 16px;
margin-bottom: 12px;
color: @color-font;
color: var(--at-color-font);
}
.atk-desc {
font-size: 14px;
margin-top: 12px;
margin-top: 10px;
margin-left: 15px;
margin-bottom: 20px;
margin-bottom: 25px;
display: block;
color: @color-meta;
color: var(--at-color-meta);
a {
opacity: 0.8;
@@ -308,24 +306,41 @@
padding: 0 10px;
margin: 0;
border: none;
border-bottom: 2px solid @color-border;
border-bottom: 2px solid var(--at-color-border);
outline: none;
width: 100%;
margin-bottom: 25px;
background: @color-bg;
background: var(--at-color-bg);
&:focus {
border-bottom-color: @color-bg-light;
border-bottom-color: var(--at-color-bg-light);
}
&[type="file"] {
border-radius: 2px;
background: transparent;
border-bottom: 2px solid @color-border;
border: 2px solid var(--at-color-border);
cursor: pointer;
position: relative;
padding: 0 15px;
margin-bottom: 30px;
margin-bottom: 20px;
&:hover {
background: var(--at-color-bg-light);
border-color: var(--at-color-bg-light);
}
}
&[type="file"]::-webkit-file-upload-button {
background: transparent;
color: #000;
border: 0;
border-right: 2px solid #eee;
padding: 0 20px 0 5px;
line-height: 25px;
margin-right: 15px;
cursor: pointer;
transition: .2s ease;
}
}
@@ -343,22 +358,22 @@
padding: 5px 10px;
width: 100%;
border: none;
border-bottom: 2px solid @color-border;
border-bottom: 2px solid var(--at-color-border);
margin-bottom: 25px;
-webkit-appearance: none;
border-radius: 0;
background: @color-bg;
background: var(--at-color-bg);
}
button {
&:hover {
background: @color-bg-light;
background: var(--at-color-bg-light);
}
text-align: center;
color: @color-light;
color: var(--at-color-light);
background: transparent;
border: 2px solid @color-bg-light;
border: 2px solid var(--at-color-bg-light);
line-height: @form-height;
letter-spacing: 2em;
text-indent: 2em; // center
@@ -381,7 +396,7 @@
padding: 5px 33px;
cursor: pointer;
user-select: none;
border-right: 1px solid @color-border;
border-right: 1px solid var(--at-color-border);
border-left: 1px solid transparent;
&:hover {
background: #f4f4f4;
@@ -409,8 +424,8 @@
width: 100%;
bottom: 0;
left: 0;
background: @color-bg;
border-top: 1px solid @color-border;
background: var(--at-color-bg);
border-top: 1px solid var(--at-color-border);
}
.atk-list-read-more {
@@ -25,7 +25,7 @@
border-radius: 2px;
&:hover {
background: @color-bg-grey;
background: var(--at-color-bg-grey);
}
}
}
@@ -58,7 +58,7 @@
height: 65px;
width: 65px;
line-height: 65px;
background: @color-quote;
background: #687a86;
color: #fff;
border-radius: 4px;
}
@@ -66,25 +66,25 @@
.atk-site-name {
text-align: center;
font-size: 15px;
color: @color-sub;
color: var(--at-color-sub);
padding: 0 17px;
word-break: break-word;
}
&.atk-active {
background-color: @color-bg-grey;
border: 1px solid @color-border;
background-color: var(--at-color-bg-grey);
border: 1px solid var(--at-color-border);
margin-top: -1px;
border-radius: 0 0 4px 4px;
.atk-site-name {
color: @color-deep;
color: var(--at-color-deep);
}
}
&:hover {
.atk-site-name {
color: @color-font;
color: var(--at-color-font);
}
}
}
@@ -94,8 +94,8 @@
position: relative;
min-height: 120px;
width: 100%;
border-top: 1px solid @color-border;
border-bottom: 1px solid @color-border;
border-top: 1px solid var(--at-color-border);
border-bottom: 1px solid var(--at-color-border);
margin-bottom: -10px;
.atk-header {
@@ -117,7 +117,7 @@
position: absolute;
width: 100%;
height: 6px;
background: @color-main;
background: var(--at-color-main);
opacity: .4;
left: 0;
bottom: 6px;
@@ -132,8 +132,8 @@
min-height: 23px;
.atk-url-item {
background: @color-bg-grey;
color: @color-font;
background: var(--at-color-bg-grey);
color: var(--at-color-font);
border-radius: 2px;
padding: 0 8px;
font-size: 13px;
@@ -156,7 +156,7 @@
cursor: pointer;
&:hover i {
background-color: @color-red;
background-color: var(--at-color-red);
}
}
}
File renamed without changes.
+3
View File
@@ -0,0 +1,3 @@
import ArtalkSidebar from '../src/artalk-sidebar'
export default ArtalkSidebar
File renamed without changes.
+48
View File
@@ -0,0 +1,48 @@
import { defineConfig } from 'vite'
import { resolve } from 'path'
import tsconfigPaths from 'vite-tsconfig-paths'
import { version as artalkVersion } from 'artalk/package.json'
import { version as sidebarVersion } from './package.json'
export default defineConfig({
base: './',
define: {
ARTALK_VERSION: JSON.stringify(artalkVersion),
ARTALK_SIDEBAR_VERSION: JSON.stringify(sidebarVersion)
},
build: {
target: 'es2015',
outDir: resolve(__dirname, "dist"),
minify: 'terser',
// lib: {
// entry: resolve(__dirname, 'src/main.ts'),
// name: 'ArtalkSidebar',
// fileName: (format) => ((format == "umd") ? 'ArtalkSidebar.js' : `ArtalkSidebar.${format}.js`),
// formats: ["es", "umd", "iife"]
// },
rollupOptions: {
// 确保外部化处理那些你不想打包进库的依赖
// external: ['artalk'],
output: {
assetFileNames: assetInfo => {
if (/\.css$/.test(assetInfo.name)) {
return 'ArtalkSidebar.css'
}
return "[name].[ext]"
},
// 在 UMD 构建模式下为这些外部化的依赖提供一个全局变量
globals: {
artalk: 'Artalk'
}
}
}
},
plugins: [tsconfigPaths()],
css: {
preprocessorOptions: {
less: {
additionalData: `@import "./src/style/_variables.less";@import "./src/style/_extend.less";`
},
},
},
})
View File
File renamed without changes.
+6 -6
View File
@@ -31,9 +31,9 @@
</style>
</head>
<body>
<div class="card billboard artalk-fade-in">
<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">一款简洁的自托管评论系统</div>
<div class="desc">一款简洁的自托管评论系统 <span id="ArtalkPV"></span></div>
<div class="social">
<a href="https://www.npmjs.com/package/artalk" target="_blank"><img src="https://img.shields.io/npm/v/artalk.svg?style=flat-square"></a>
<a href="https://www.npmjs.com/package/artalk" target="_blank"><img src="https://img.shields.io/npm/dt/artalk.svg?style=flat-square"></a>
@@ -42,11 +42,11 @@
<a href="https://github.com/ArtalkJS/Artalk" target="_blank"><img src="https://img.shields.io/github/stars/ArtalkJS/Artalk?style=flat-square"></a>
</div>
</div>
<div class="card artalk-fade-in">
<div class="card atk-fade-in">
<div id="comments"></div>
</div>
<div class="float-btn to-night artalk-fade-in">Blink</div>
<div class="float-btn to-top artalk-fade-in" style="display: none;">TOP</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'
@@ -54,7 +54,7 @@
el: '#comments',
pageKey: 'https://artalk.js.org/',
pageTitle: 'Artalk Home',
server: (location.hostname === 'localhost' || location.hostname === '127.0.0.1') ? 'http://localhost:23366/api' : 'https://artalk.qwqaq.com/api',
server: (import.meta.env.MODE === 'development') ? 'http://localhost:23366/api' : 'https://artalk.qwqaq.com/api',
site: 'ArtalkDemo',
});
+43
View File
@@ -0,0 +1,43 @@
{
"name": "artalk",
"version": "2.1.7",
"license": "GPL-3.0",
"description": "A Selfhosted Comment System",
"keywords": [
"comment-system",
"comments-widget",
"comment",
"comments",
"blog"
],
"repository": {
"type": "git",
"url": "https://github.com/ArtalkJS/Artalk.git",
"directory": "packages/artalk"
},
"homepage": "https://artalk.js.org",
"bugs": "https://github.com/ArtalkJS/Artalk/issue",
"main": "./dist/Artalk.js",
"unpkg": "./dist/Artalk.iife.js",
"jsdelivr": "./dist/Artalk.iife.js",
"module": "./dist/Artalk.es.js",
"types": "./types/index.d.ts",
"scripts": {
"dev": "vite",
"build": "yarn lint && vite build",
"build:demo": "vite build --config vite-demo.config.ts",
"serve": "vite preview",
"deploy": "gh-pages -d deploy",
"predeploy": "yarn run build && yarn run build:demo && yarn run copy:deploy",
"copy:deploy": "cp -r dist deploy && cp CNAME deploy",
"lint": "eslint --ext .js,.ts src"
},
"devDependencies": {
"@types/marked": "^4.0.3"
},
"dependencies": {
"hanabi": "^0.4.0",
"insane": "^2.6.2",
"marked": "^4.0.14"
}
}
File renamed without changes.
@@ -1,6 +1,7 @@
import Context from '../context'
import { CommentData, ListData, UserData, PageData, SiteData, NotifyData } from '~/types/artalk-data'
import { Fetch, ToFormData, POST, GET } from './request'
import * as Utils from '../lib/utils'
export default class Api {
private ctx: Context
@@ -16,15 +17,14 @@ export default class Api {
// ============================
/** 评论 · 获取 */
public get(offset: number, type?: string, flatMode?: boolean, paramsEditor?: (params: any) => void) {
public get(offset: number, pageSize: number, flatMode?: boolean, paramsEditor?: (params: any) => void) {
const params: any = {
page_key: this.ctx.conf.pageKey,
site_name: this.ctx.conf.site || '',
limit: this.ctx.conf.pagination?.pageSize || 15,
limit: pageSize,
offset,
}
if (type) params.type = type
if (flatMode) params.flat_mode = flatMode // 平铺模式
if (this.ctx.user.checkHasBasicUserInfo()) {
params.name = this.ctx.user.data.nick
@@ -45,6 +45,7 @@ export default class Api {
content: comment.content,
rid: comment.rid,
page_key: comment.page_key,
ua: await Utils.getCorrectUserAgent(), // 需要后端支持,获取修正后的 UA (针对 Win11)
}
if (comment.page_title) params.page_title = comment.page_title
@@ -251,6 +252,18 @@ export default class Api {
return POST(this.ctx, `${this.baseURL}/mark-read`, params)
}
/** PV */
public async pv() {
const params: any = {
site_name: this.ctx.conf.site || '',
page_key: this.ctx.conf.pageKey || '',
page_title: this.ctx.conf.pageTitle || ''
}
const p = await POST<any>(this.ctx, `${this.baseURL}/pv`, params)
return p.pv as number
}
// ============================
// 验证码
// ============================
@@ -21,7 +21,9 @@ export async function Fetch(ctx: Context, input: RequestInfo, init: RequestInit,
resp = await timeoutPromise(timeout || ctx.conf.reqTimeout || 15000, fetch(input, init))
}
if (!resp.ok && resp.status !== 401) throw new Error(`请求响应 ${resp.status}`)
const noAccessCodes = [401, 400]
if (!resp.ok && !noAccessCodes.includes(resp.status)) throw new Error(`请求响应 ${resp.status}`)
// 解析获取响应的 json
let json: any = await resp.json()
@@ -49,7 +51,7 @@ export async function Fetch(ctx: Context, input: RequestInfo, init: RequestInit,
}
})
}))
} else if ((json.data && json.data.need_login) || resp.status === 401) {
} else if ((json.data && json.data.need_login) || noAccessCodes.includes(resp.status)) {
// 请求需要管理员权限
json = await (new Promise<any>((resolve, reject) => {
ctx.trigger('checker-admin', {
@@ -7,10 +7,11 @@ import defaults from './defaults'
import CheckerLauncher from './lib/checker'
import Editor from './components/editor'
import List from './components/list'
import Sidebar from './components/sidebar'
import SidebarLayer from './components/sidebar-layer'
import { GetLayerWrap } from './components/layer'
import { EventPayloadMap, Handler } from '~/types/event'
import Api from './api'
/**
* Artalk
@@ -26,7 +27,7 @@ export default class Artalk {
public checkerLauncher: CheckerLauncher
public editor: Editor
public list: List
public sidebar: Sidebar
public sidebarLayer: SidebarLayer
constructor (customConf: ArtalkConfig) {
// 配置
@@ -68,14 +69,17 @@ export default class Artalk {
this.$root.appendChild(this.list.$el)
// 侧边栏
this.sidebar = new Sidebar(this.ctx)
this.$root.appendChild(this.sidebar.$el)
this.sidebarLayer = new SidebarLayer(this.ctx)
this.$root.appendChild(this.sidebarLayer.$el)
// 评论获取
this.list.reqComments()
this.list.fetchComments(0)
// 事件绑定初始化
this.initEventBind()
// 其他
this.initPV()
}
/** 事件绑定 · 初始化 */
@@ -93,8 +97,12 @@ export default class Artalk {
// for layer
const { $wrap: $layerWrap } = GetLayerWrap(this.ctx)
if ($layerWrap)
$layerWrap.querySelectorAll<HTMLElement>(`[atk-only-admin-show]`).forEach(item => items.push(item))
if ($layerWrap) $layerWrap.querySelectorAll<HTMLElement>(`[atk-only-admin-show]`).forEach(item => items.push(item))
// for sidebar
// TODO: 这个其实应该写在 packages/artalk-sidebar 里面的
const $sidebarEl = document.querySelector<HTMLElement>('.atk-sidebar')
if ($sidebarEl) $sidebarEl.querySelectorAll<HTMLElement>(`[atk-only-admin-show]`).forEach(item => items.push(item))
items.forEach(($item: HTMLElement) => {
if (this.ctx.user.data.isAdmin)
@@ -113,7 +121,7 @@ export default class Artalk {
/** 重新加载 */
public reload() {
this.list.reqComments()
this.list.fetchComments(0)
}
/** 暗黑模式 · 初始化 */
@@ -143,6 +151,20 @@ export default class Artalk {
this.initDarkMode()
}
/** PV */
public async initPV() {
if (!this.conf.pvEl || !document.querySelector(this.conf.pvEl))
return
const $pv = document.querySelector<HTMLElement>(this.conf.pvEl)!
// $pv.innerText = '-' // 默认占位符
const pvNum = await new Api(this.ctx).pv()
if (Number.isNaN(Number(pvNum)))
return
$pv.innerText = String(pvNum)
}
/** 监听事件 */
public on<K extends keyof EventPayloadMap>(name: K, handler: Handler<EventPayloadMap[K]>) {
this.ctx.on(name, handler, 'external')
@@ -102,7 +102,7 @@ export default class ActionBtn {
}
/** 设置加载状态 */
public setLoading(value = true, loadingText?: string) {
public setLoading(value: boolean, loadingText?: string) {
if (this.isLoading === value) return
this.isLoading = value
if (value) {
@@ -55,11 +55,11 @@ export default class Comment extends Component {
/** 渲染 UI */
public render() {
this.$el = Utils.createElement(CommentHTML)
this.$main = this.$el.querySelector('.atk-comment-main')!
this.$main = this.$el.querySelector('.atk-main')!
this.$header = this.$el.querySelector('.atk-header')!
this.$body = this.$el.querySelector('.atk-body')!
this.$content = this.$body.querySelector('.atk-content')!
this.$actions = this.$el.querySelector('.atk-comment-actions')!
this.$actions = this.$el.querySelector('.atk-actions')!
this.$children = null
this.$el.setAttribute('data-comment-id', `${this.data.id}`)
@@ -136,6 +136,11 @@ export default class Comment extends Component {
$badge.remove()
}
if (this.data.is_pinned) {
const $pinnedBadge = Utils.createElement(`<span class="atk-item atk-pinned-badge">置顶</span>`) // 置顶徽章
$nick.insertAdjacentElement('afterend', $pinnedBadge)
}
const $date = this.$el.querySelector<HTMLElement>('.atk-date')!
$date.innerText = this.getDateFormatted()
$date.setAttribute('data-atk-comment-date', String(+new Date(this.data.date)))
@@ -229,7 +234,7 @@ export default class Comment extends Component {
// 绑定回复按钮事件
if (this.data.is_allow_reply) {
const replyBtn = Utils.createElement(`<span data-atk-action="comment-reply">回复</span>`)
const replyBtn = Utils.createElement(`<span>回复</span>`)
this.$actions.append(replyBtn)
replyBtn.addEventListener('click', (e) => {
e.stopPropagation() // 防止穿透
@@ -272,6 +277,16 @@ export default class Comment extends Component {
delBtn.setClick(() => {
this.adminDelete(delBtn)
})
// 绑定置顶按钮事件
const pinnedBtn = new ActionBtn({
text: () => (this.data.is_pinned ? '取消置顶' : '置顶'),
adminOnly: true
})
pinnedBtn.appendTo(this.$actions)
pinnedBtn.setClick(() => {
this.adminEdit('pinned', pendingBtn)
})
}
//#endregion
@@ -316,6 +331,9 @@ export default class Comment extends Component {
this.getChildrenEl().appendChild(childC.getEl())
childC.playFadeInAnim()
// 内容限高
childC.checkHeightLimitArea('content')
}
getChildrenEl() {
@@ -401,9 +419,9 @@ export default class Comment extends Component {
})
}
/** 管理员 - 评论折叠 */
adminEdit(type: 'collapsed'|'pending', btnElem: ActionBtn) {
if (btnElem.isLoading) return // 若正在折叠中
/** 管理员 - 评论状态修改 */
adminEdit(type: 'collapsed'|'pending'|'pinned', btnElem: ActionBtn) {
if (btnElem.isLoading) return // 若正在修改中
btnElem.setLoading(true, '修改中...')
@@ -413,6 +431,8 @@ export default class Comment extends Component {
modify.is_collapsed = !modify.is_collapsed
} else if (type === 'pending') {
modify.is_pending = !modify.is_pending
} else if (type === 'pinned') {
modify.is_pinned = !modify.is_pinned
}
new Api(this.ctx).commentEdit(modify).then((comment) => {
@@ -465,4 +485,86 @@ export default class Comment extends Component {
this.openURL = url
this.$el.classList.add('atk-openable')
}
/** 内容限高检测 */
checkHeightLimit() {
this.checkHeightLimitArea('content') // 评论内容限高
this.checkHeightLimitArea('children') // 子评论部分限高(嵌套模式)
}
/** 目标内容限高检测 */
checkHeightLimitArea(area: 'children'|'content') {
// 参数准备
const childrenMaxH = this.ctx.conf.heightLimit?.children
const contentMaxH = this.ctx.conf.heightLimit?.content
if (area === 'children' && !childrenMaxH) return
if (area === 'content' && !contentMaxH) return
// 限高
let maxHeight: number
if (area === 'children') maxHeight = childrenMaxH!
if (area === 'content') maxHeight = contentMaxH!
// 检测指定元素
const checkEl = ($el?: HTMLElement|null) => {
if (!$el) return
// 是否超过高度
if (Utils.getHeight($el) > maxHeight) {
this.heightLimitAdd($el, maxHeight)
}
}
// 执行限高检测
if (area === 'children') {
checkEl(this.$children)
} else if (area === 'content') {
checkEl(this.$content)
checkEl(this.$replyTo)
// 若有图片 · 图片加载完后再检测一次
Utils.onImagesLoaded(this.$content, () => {
checkEl(this.$content)
})
if (this.$replyTo) {
Utils.onImagesLoaded(this.$replyTo, () => {
checkEl(this.$replyTo)
})
}
}
}
// 操作 · 取消限高
heightLimitRemove($el: HTMLElement) {
if (!$el) return
if (!$el.classList.contains('atk-height-limit')) return
$el.classList.remove('atk-height-limit')
Array.from($el.children).forEach((e) => {
if (e.classList.contains('atk-height-limit-btn')) e.remove()
})
$el.style.height = ''
$el.style.overflow = ''
}
// 操作 · 内容限高
heightLimitAdd($el: HTMLElement, maxHeight: number) {
if (!$el) return
if ($el.classList.contains('atk-height-limit')) return
$el.classList.add('atk-height-limit')
$el.style.height = `${maxHeight}px`
$el.style.overflow = 'hidden'
const $hideMoreOpenBtn = Utils.createElement(`<div class="atk-height-limit-btn">阅读更多</span>`)
$hideMoreOpenBtn.onclick = (e) => {
e.stopPropagation()
this.heightLimitRemove($el)
// 子评论数等于 1,直接取消限高
const children = this.getChildren()
if (children.length === 1) children[0].heightLimitRemove(children[0].$content)
}
$el.append($hideMoreOpenBtn)
}
}
File renamed without changes.
@@ -2,44 +2,55 @@
height: 100%;
width: 100%;
.atk-emoticons-list-wrap {
& > .atk-grp-wrap {
.atk-slim-scrollbar();
overflow-y: scroll;
overflow-x: hidden;
height: 100%; // calc(100% - 30px);
width: 100%;
.atk-emoticons-list {
& > .atk-grp {
display: flex;
flex-wrap: wrap;
flex-direction: row;
padding: 5px 10px 35px 10px;
.atk-emoticons-item {
display: inline-block;
&[data-type="image"] > .atk-item {
height: 63px;
width: 63px;
}
& > .atk-item {
display: flex;
align-items: center;
justify-content: center;
padding: 5px;
cursor: pointer;
user-select: none;
border-radius: 2px;
font-size: 16px;
min-width: 35px;
& > img {
max-height: 60px;
max-height: 100%;
width: auto;
}
&:hover {
background: @color-bg-grey;
background: var(--at-color-bg-grey);
}
}
}
}
.atk-emoticons-types {
& > .atk-grp-switcher {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
background: @color-bg-transl;
background: var(--at-color-bg-transl);
height: 30px;
border-top: 1px solid @color-border;
border-top: 1px solid var(--at-color-border);
& > span {
user-select: none;
@@ -52,9 +63,8 @@
&:hover,
&.active {
background: @color-bg-grey;
background: var(--at-color-bg-grey);
}
}
}
}
@@ -17,8 +17,8 @@ export default class EmoticonsPlug extends EditorPlug {
public $el!: HTMLElement
public emoticons: EmoticonListData = []
public $listWrap!: HTMLElement
public $types!: HTMLElement
public $grpWrap!: HTMLElement
public $grpSwitcher!: HTMLElement
constructor (public editor: Editor) {
super(editor)
@@ -162,18 +162,18 @@ export default class EmoticonsPlug extends EditorPlug {
/** 初始化表情列表界面 */
initEmoticonsList () {
// 表情列表
this.$listWrap = Utils.createElement(`<div class="atk-emoticons-list-wrap"></div>`)
this.$el.append(this.$listWrap)
this.$grpWrap = Utils.createElement(`<div class="atk-grp-wrap"></div>`)
this.$el.append(this.$grpWrap)
this.emoticons.forEach((grp, index) => {
const emoticonsEl = Utils.createElement(`<div class="atk-emoticons-list" style="display: none;"></div>`)
this.$listWrap.append(emoticonsEl)
emoticonsEl.setAttribute('data-index', String(index))
emoticonsEl.setAttribute('data-grp-name', grp.name)
emoticonsEl.setAttribute('data-type', grp.type)
const $grp = Utils.createElement(`<div class="atk-grp" style="display: none;"></div>`)
this.$grpWrap.append($grp)
$grp.setAttribute('data-index', String(index))
$grp.setAttribute('data-grp-name', grp.name)
$grp.setAttribute('data-type', grp.type)
grp.items.forEach((item) => {
const $item = Utils.createElement(`<span class="atk-emoticons-item"></span>`)
emoticonsEl.append($item)
const $item = Utils.createElement(`<span class="atk-item"></span>`)
$grp.append($item)
if (!!item.key && !(new RegExp(`^(${grp.name})?\\s?[0-9]+$`).test(item.key)))
$item.setAttribute('title', item.key)
@@ -198,14 +198,14 @@ export default class EmoticonsPlug extends EditorPlug {
})
// 表情分类
this.$types = Utils.createElement(`<div class="atk-emoticons-types"></div>`)
this.$el.append(this.$types)
this.$grpSwitcher = Utils.createElement(`<div class="atk-grp-switcher"></div>`)
this.$el.append(this.$grpSwitcher)
this.emoticons.forEach((grp, index) => {
const $item = Utils.createElement('<span />')
$item.innerText = grp.name
$item.setAttribute('data-index', String(index))
$item.onclick = () => (this.openGrp(index))
this.$types.append($item)
this.$grpSwitcher.append($item)
})
// 默认打开第一个分类
@@ -215,7 +215,7 @@ export default class EmoticonsPlug extends EditorPlug {
/** 打开一个表情组 */
openGrp (index: number) {
Array.from(this.$listWrap.children).forEach((item) => {
Array.from(this.$grpWrap.children).forEach((item) => {
const el = item as HTMLElement
if (el.getAttribute('data-index') !== String(index)) {
el.style.display = 'none'
@@ -224,8 +224,8 @@ export default class EmoticonsPlug extends EditorPlug {
}
})
this.$types.querySelectorAll('span.active').forEach(item => item.classList.remove('active'))
this.$types.querySelector(`span[data-index="${index}"]`)?.classList.add('active')
this.$grpSwitcher.querySelectorAll('span.active').forEach(item => item.classList.remove('active'))
this.$grpSwitcher.querySelector(`span[data-index="${index}"]`)?.classList.add('active')
this.changeListHeight()
}
@@ -238,7 +238,7 @@ export default class EmoticonsPlug extends EditorPlug {
}
changeListHeight () {
/* const listWrapHeight = Utils.getHeight(this.$listWrapem)
/* const listWrapHeight = Utils.getHeight(this.$grpWrapem)
this.editor.plugWrapEl.style.height = `${listWrapHeight > 150 ? listWrapHeight : 150}px` */
}
@@ -3,5 +3,5 @@
overflow-x: hidden;
height: 100%;
width: 100%;
padding: @block-padding 10px + 5px;
padding: 10px (10px + 5px);
}
@@ -18,12 +18,9 @@ export default class Editor extends Component {
public $header: HTMLElement
public $textareaWrap: HTMLElement
public $textarea: HTMLTextAreaElement
public $closeComment: HTMLTextAreaElement
public $plugWrap: HTMLElement
public $bottom: HTMLElement
public $bottomPartLeft: HTMLElement
public $plugSwitcherWrap: HTMLElement
public $bottomPartRight: HTMLElement
public $plugBtnWrap: HTMLElement
public $submitBtn: HTMLButtonElement
public $notifyWrap: HTMLElement
@@ -41,17 +38,14 @@ export default class Editor extends Component {
this.$el = Utils.createElement(EditorHTML)
this.$header = this.$el.querySelector('.atk-editor-header')!
this.$textareaWrap = this.$el.querySelector('.atk-editor-textarea-wrap')!
this.$textarea = this.$el.querySelector('.atk-editor-textarea')!
this.$closeComment = this.$el.querySelector('.atk-close-comment')!
this.$plugWrap = this.$el.querySelector('.atk-editor-plug-wrap')!
this.$bottom = this.$el.querySelector('.atk-editor-bottom')!
this.$bottomPartLeft = this.$el.querySelector('.atk-editor-bottom-part.atk-left')!
this.$plugSwitcherWrap = this.$el.querySelector('.atk-editor-plug-switcher-wrap')!
this.$bottomPartRight = this.$el.querySelector('.atk-editor-bottom-part.atk-right')!
this.$header = this.$el.querySelector('.atk-header')!
this.$textareaWrap = this.$el.querySelector('.atk-textarea-wrap')!
this.$textarea = this.$el.querySelector('.atk-textarea')!
this.$plugWrap = this.$el.querySelector('.atk-plug-wrap')!
this.$bottom = this.$el.querySelector('.atk-bottom')!
this.$plugBtnWrap = this.$el.querySelector('.atk-plug-btn-wrap')!
this.$submitBtn = this.$el.querySelector('.atk-send-btn')!
this.$notifyWrap = this.$el.querySelector('.atk-editor-notify-wrap')!
this.$notifyWrap = this.$el.querySelector('.atk-notify-wrap')!
this.initLocalStorage()
this.initHeader()
@@ -201,13 +195,13 @@ export default class Editor extends Component {
this.$plugWrap.innerHTML = ''
this.$plugWrap.style.display = 'none'
this.openedPlugName = null
this.$plugSwitcherWrap.innerHTML = ''
this.$plugBtnWrap.innerHTML = ''
// 依次实例化 plug
this.LOADABLE_PLUG_LIST.forEach((PlugObj) => {
// 切换按钮
const btnElem = Utils.createElement(`<span class="atk-editor-action atk-editor-plug-switcher">${PlugObj.BtnHTML}</span>`)
this.$plugSwitcherWrap.appendChild(btnElem)
const btnElem = Utils.createElement(`<span class="atk-plug-btn">${PlugObj.BtnHTML}</span>`)
this.$plugBtnWrap.appendChild(btnElem)
btnElem.addEventListener('click', () => {
let plug = this.plugList[PlugObj.Name]
if (!plug) {
@@ -215,7 +209,7 @@ export default class Editor extends Component {
this.plugList[PlugObj.Name] = plug
}
this.$plugSwitcherWrap.querySelectorAll('.active').forEach(item => item.classList.remove('active'))
this.$plugBtnWrap.querySelectorAll('.active').forEach(item => item.classList.remove('active'))
// 若点击已打开的,则收起
if (PlugObj.Name === this.openedPlugName) {
@@ -413,7 +407,8 @@ export default class Editor extends Component {
/** 关闭评论 */
close () {
this.$closeComment.style.display = ''
if (!this.$textareaWrap.querySelector('.atk-comment-closed'))
this.$textareaWrap.prepend(Utils.createElement('<div class="atk-comment-closed">仅管理员可评论</div>'))
if (!this.user.data.isAdmin) {
this.$textarea.style.display = 'none'
@@ -428,7 +423,7 @@ export default class Editor extends Component {
/** 打开评论 */
open () {
this.$closeComment.style.display = 'none'
this.$textareaWrap.querySelector('.atk-comment-closed')?.remove()
this.$textarea.style.display = ''
this.$bottom.style.display = ''
}
@@ -1,25 +1,18 @@
<div class="atk-comment-wrap" data-comment-id="">
<div class="atk-comment-wrap">
<div class="atk-comment">
<div class="atk-avatar"></div>
<div class="atk-comment-main">
<div class="atk-main">
<div class="atk-header">
<span class="atk-nick"></span>
<span class="atk-badge"></span>
<span class="atk-date"></span>
<span class="atk-item atk-nick"></span>
<span class="atk-item atk-badge"></span>
<span class="atk-item atk-date"></span>
</div>
<div class="atk-body">
<div class="atk-content"></div>
</div>
<div class="atk-footer">
<div class="atk-comment-actions"></div>
<div class="atk-actions"></div>
</div>
</div>
</div>
</div>
@@ -0,0 +1,18 @@
<div class="atk-main-editor">
<div class="atk-header">
<input name="nick" placeholder="昵称" class="atk-nick" type="text" required="required">
<input name="email" placeholder="邮箱" class="atk-email" type="email" required="required">
<input name="link" placeholder="网址 (https://)" class="atk-link" type="url">
</div>
<div class="atk-textarea-wrap">
<textarea class="atk-textarea" placeholder=""></textarea>
</div>
<div class="atk-plug-wrap" style="display: none;"></div>
<div class="atk-bottom">
<div class="atk-item atk-plug-btn-wrap"></div>
<div class="atk-item">
<button type="button" class="atk-send-btn"></button>
</div>
</div>
<div class="atk-notify-wrap"></div>
</div>
@@ -1,14 +1,16 @@
<div class="atk-list">
<div class="atk-list-header">
<div class="atk-comment-count">
<span class="atk-comment-count-num">0</span>
条评论
<div class="atk-text">
<span class="atk-comment-count-num">0</span>
条评论
</div>
</div>
<div class="atk-right-action">
<span data-action="admin-close-comment" class="atk-hide" atk-only-admin-show>关闭评论</span>
<span data-action="open-sidebar" class="atk-hide atk-on">
<span class="atk-unread-badge" style="display: none;"></span>
通知中心
<div class="atk-text">通知中心</div>
</span>
</div>
</div>
@@ -0,0 +1,8 @@
<div class="atk-sidebar-layer">
<div class="atk-sidebar-inner">
<div class="atk-sidebar-header">
<div class="atk-sidebar-close"><i class="atk-icon atk-icon-close"></i></div>
</div>
<div class="atk-sidebar-iframe-wrap"></div>
</div>
</div>
File renamed without changes.
+492
View File
@@ -0,0 +1,492 @@
import Context from '../context'
import Component from '../lib/component'
import * as Utils from '../lib/utils'
import * as Ui from '../lib/ui'
import Api from '../api'
import Comment from './comment'
import { ListData, CommentData, NotifyData, ApiVersionData } from '~/types/artalk-data'
import Pagination from './pagination'
import ReadMoreBtn from './read-more-btn'
export default class ListLite extends Component {
protected $commentsWrap: HTMLElement
protected comments: Comment[] = []
protected data?: ListData
protected isLoading: boolean = false
public noCommentText = '无内容' // 无评论时显示
/** 平铺模式 */
public flatMode = false
/** 分页 */
public pageMode: 'pagination'|'read-more' = 'pagination'
public pageSize: number = 20 // 每次请求获取量
public scrollListenerAt?: HTMLElement // 监听指定元素上的滚动
public repositionAt?: HTMLElement // 翻页归位到指定元素
protected pagination?: Pagination // 分页条
protected readMoreBtn?: ReadMoreBtn // 阅读更多
protected autoLoadScrollEvent?: any // 自动加载滚动事件
public renderComment?: (comment: Comment) => void
public paramsEditor?: (params: any) => void
public onAfterLoad?: (data: ListData) => void
// 未读标记
protected unread: NotifyData[] = []
public unreadHighlight?: boolean // 高亮未读
constructor (ctx: Context) {
super(ctx)
// 初始化元素
this.$el = Utils.createElement(
`<div class="atk-list-lite">
<div class="atk-list-comments-wrap"></div>
</div>`)
this.$commentsWrap = this.$el.querySelector('.atk-list-comments-wrap')!
// 评论时间自动更新
window.setInterval(() => {
this.$el.querySelectorAll<HTMLElement>('[data-atk-comment-date]').forEach(el => {
const date = el.getAttribute('data-atk-comment-date')
el.innerText = Utils.timeAgo(new Date(Number(date)))
})
}, 30 * 1000) // 30s 更新一次
// 事件监听
this.ctx.on('unread-update', (data) => (this.updateUnread(data.notifies)))
}
/** 评论获取 */
public async fetchComments(offset: number) {
if (this.isLoading) return
// 加载动画
const showLoading = () => {
this.isLoading = true
if (offset === 0) Ui.showLoading(this.$el)
else if (this.pageMode === 'read-more') this.readMoreBtn!.setLoading(true)
else if (this.pageMode === 'pagination') this.pagination!.setLoading(true)
}
const hideLoading = () => {
this.isLoading = false
if (offset === 0) Ui.hideLoading(this.$el)
else if (this.pageMode === 'read-more') this.readMoreBtn!.setLoading(false)
else if (this.pageMode === 'pagination') this.pagination!.setLoading(false)
}
showLoading()
// 事件通知(开始加载评论)
this.ctx.trigger('comments-load')
// 清空评论(加载按钮)
if (this.pageMode === 'read-more' && offset === 0) { this.clearAllComments() }
// 请求评论数据
let listData: ListData
try {
listData = await new Api(this.ctx).get(offset, this.pageSize, this.flatMode, this.paramsEditor)
} catch (e: any) {
this.onError(e.msg || String(e), offset)
throw e
} finally {
hideLoading()
}
// 清除原有错误
Ui.setError(this.$el, null)
// 装载数据
try {
this.onLoad(listData, offset)
} catch (e: any) {
this.onError(String(e), offset)
throw e
} finally {
hideLoading()
}
}
protected onLoad(data: ListData, offset: number) {
// 清空评论(分页条)
if (this.pageMode === 'pagination') { this.clearAllComments() }
this.data = data
// 版本检测
if (this.ctx.conf.versionCheck && this.versionCheck(data.api_version)) return
// 导入数据
this.importComments(data.comments)
// 分页方式
this.refreshPagination(offset, (this.flatMode ? data.total : data.total_roots)) // 初始化
// 加载后事件
this.refreshUI()
this.ctx.trigger('unread-update', { notifies: data.unread || [] })
this.ctx.trigger('comments-loaded')
if (this.onAfterLoad) this.onAfterLoad(data)
}
/** 分页模式 */
private refreshPagination(offset: number, total: number) {
const modePagination = (this.pageMode === 'pagination')
const modeReadMoreBtn = (this.pageMode === 'read-more')
const initialized = (modePagination) ? !!this.pagination : !!this.readMoreBtn
// 初始化
if (!initialized) {
this.initPagination()
}
// 更新
if (modePagination) this.pagination!.update(offset, total)
if (modeReadMoreBtn) this.readMoreBtn!.update(offset, total)
}
private initPagination() {
// 加载更多按钮
if (this.pageMode === 'read-more') {
this.readMoreBtn = new ReadMoreBtn({
pageSize: this.pageSize,
onClick: async (o) => {
await this.fetchComments(o)
},
})
this.$el.append(this.readMoreBtn.$el)
// 滚动到底部自动加载
if (this.conf.pagination?.autoLoad) {
// 添加滚动事件监听
const at = this.scrollListenerAt || document
if (this.autoLoadScrollEvent) at.removeEventListener('scroll', this.autoLoadScrollEvent) // 解除原有
this.autoLoadScrollEvent = () => {
if (this.pageMode !== 'read-more') return
if (!this.readMoreBtn) return
if (!this.readMoreBtn.hasMore) return
if (this.isLoading) return
const $target = this.$el.querySelector<HTMLElement>('.atk-list-comments-wrap > .atk-comment-wrap:nth-last-child(3)') // 获取倒数第3个评论元素
if (!$target) return
if (Ui.isVisible($target, this.scrollListenerAt)) {
this.readMoreBtn.click() // 自动点击加载更多按钮
}
}
at.addEventListener('scroll', this.autoLoadScrollEvent)
}
}
// 分页条
if (this.pageMode === 'pagination') {
this.pagination = new Pagination((!this.flatMode ? this.data!.total_roots : this.data!.total), {
pageSize: this.pageSize,
onChange: async (o) => {
if (this.ctx.conf.editorTravel === true) {
this.ctx.trigger('editor-travel-back') // 防止评论框被吞
}
await this.fetchComments(o)
// 滚动到第一个评论的位置
if (this.repositionAt) {
const at = this.scrollListenerAt || window
at.scroll({
top: this.repositionAt ? Utils.getOffset(this.repositionAt).top : 0,
left: 0,
})
}
}
})
this.$el.append(this.pagination.$el)
}
}
/** 错误处理 */
protected onError(msg: any, offset: number) {
msg = String(msg)
console.error(msg)
// 加载更多按钮显示错误
if (offset !== 0 && this.pageMode === 'read-more') {
this.readMoreBtn?.showErr(`获取失败`)
return
}
// 显示错误对话框
const $err = Utils.createElement(`<span>${msg},无法获取评论列表数据<br/></span>`)
const $retryBtn = Utils.createElement('<span style="cursor:pointer;">点击重新获取</span>')
$retryBtn.onclick = () => (this.fetchComments(0))
$err.appendChild($retryBtn)
const adminBtn = Utils.createElement('<span atk-only-admin-show> | <span style="cursor:pointer;">打开控制台</span></span>')
adminBtn.onclick = () => (this.ctx.trigger('sidebar-show'))
if (!this.ctx.user.data.isAdmin) adminBtn.classList.add('atk-hide')
$err.appendChild(adminBtn)
Ui.setError(this.$el, $err)
}
/** 刷新界面 */
public refreshUI() {
// 无评论
const isNoComment = this.comments.length <= 0
let $noComment = this.$commentsWrap.querySelector<HTMLElement>('.atk-list-no-comment')
if (isNoComment) {
if (!$noComment) {
$noComment = Utils.createElement('<div class="atk-list-no-comment"></div>')
$noComment.innerHTML = this.noCommentText
this.$commentsWrap.appendChild($noComment)
}
} else {
$noComment?.remove()
}
// 仅管理员显示控制
this.ctx.trigger('check-admin-show-el')
}
/** 创建新评论 */
protected createComment(data: CommentData) {
const comment = new Comment(this.ctx, data)
comment.afterRender = () => {
if (this.renderComment) this.renderComment(comment)
}
comment.onDelete = (c) => {
this.deleteComment(c)
this.refreshUI()
}
return comment
}
/** 导入评论 · 通过请求数据 */
public importComments(srcData: CommentData[]) {
if (this.flatMode) {
srcData.forEach((commentData: CommentData) => {
this.putCommentFlatMode(commentData, srcData, 'append')
})
} else {
this.importCommentsNesting(srcData)
}
}
// 导入评论 · 嵌套模式
private importCommentsNesting(srcData: CommentData[]) {
// 查找并导入所有子评论
const loadChildren = (parentC: Comment) => {
const children = srcData.filter(o => o.rid === parentC.data.id)
children.forEach((childData: CommentData) => {
const childC = this.createComment(childData)
childC.render()
// 插入到父评论中
parentC.putChild(childC)
// 递归加载子评论
loadChildren(childC)
})
}
// 遍历 root 评论
const rootComments = srcData.filter((o) => o.rid === 0)
rootComments.forEach((rootData: CommentData) => {
const rootC = this.createComment(rootData)
rootC.render()
// 显示并播放渐入动画
this.$commentsWrap.appendChild(rootC.getEl())
rootC.playFadeInAnim()
// 评论放入 comments 总表中
this.comments.push(rootC)
// 加载子评论
loadChildren(rootC)
// 限高检测
rootC.checkHeightLimit()
})
}
/** 导入评论 · 平铺模式 */
private putCommentFlatMode(cData: CommentData, srcData: CommentData[], insertMode: 'append'|'prepend') {
if (cData.is_collapsed) cData.is_allow_reply = false
const comment = this.createComment(cData)
if (cData.rid !== 0) {
const rComment = srcData.find(o => o.id === cData.rid)
if (rComment) comment.replyTo = rComment
}
comment.render()
// 将评论导入 comments 总表中
if (insertMode === 'append') this.comments.push(comment)
if (insertMode === 'prepend') this.comments.unshift(comment)
// 可见评论添加到界面
// 注:不可见评论用于显示 “引用内容”
if (cData.visible) {
if (insertMode === 'append') this.$commentsWrap.append(comment.getEl())
if (insertMode === 'prepend') this.$commentsWrap.prepend(comment.getEl())
comment.playFadeInAnim()
}
// 平铺评论插入后 · 内容限高检测
comment.checkHeightLimit()
return comment
}
/** 新增评论 · 首部添加 */
public insertComment(commentData: CommentData) {
if (!this.flatMode) {
// 嵌套模式
const comment = this.createComment(commentData)
comment.render()
if (commentData.rid === 0) {
// root评论 新增
this.$commentsWrap.prepend(comment.getEl())
this.comments.unshift(comment)
} else {
// 子评论 新增
const parent = this.findComment(commentData.rid)
if (parent) {
parent.putChild(comment)
// 若父评论存在 “子评论部分” 限高,取消限高
comment.getParents().forEach((p) => {
if (p.$children) p.heightLimitRemove(p.$children)
})
}
}
comment.checkHeightLimit()
Ui.scrollIntoView(comment.getEl()) // 滚动到可以见
comment.playFadeInAnim() // 播放评论渐出动画
} else {
// 平铺模式
const comment = this.putCommentFlatMode(commentData, this.comments.map(c => c.data), 'prepend')
Ui.scrollIntoView(comment.getEl()) // 滚动到可见
}
// 评论数增加 1
if (this.data) this.data.total += 1
// 评论新增后
this.refreshUI()
this.ctx.trigger('comments-loaded')
}
/** 遍历操作 Comment (包括子评论) */
protected eachComment(commentList: Comment[], action: (comment: Comment, levelList: Comment[]) => boolean|void) {
if (commentList.length === 0) return
commentList.every((item) => {
if (action(item, commentList) === false) return false
this.eachComment(item.getChildren(), action)
return true
})
}
/** 查找评论项 */
protected findComment(id: number, src?: Comment[]): Comment|null {
if (!src) src = this.comments
let comment: Comment|null = null
this.eachComment(src, (item) => {
if (item.data.id === id) {
comment = item
return false
}
return true
})
return comment
}
/** 删除评论 */
public deleteComment(comment: number|Comment) {
let findComment: Comment|null
if (typeof comment === 'number') {
findComment = this.findComment(comment)
if (!findComment) throw Error(`未找到评论 ${comment}`)
} else findComment = comment
findComment.getEl().remove()
this.eachComment(this.comments, (item, levelList) => {
if (item === findComment) {
levelList.splice(levelList.indexOf(item), 1)
return false
}
return true
})
if (this.data) this.data.total -= 1 // 评论数减 1
this.refreshUI()
}
/** 清空所有评论 */
public clearAllComments() {
this.$commentsWrap.innerHTML = ''
this.data = undefined
this.comments = []
}
/** 更新未读数据 */
public updateUnread(notifies: NotifyData[]) {
this.unread = notifies
// 高亮评论
if (this.unreadHighlight === true) {
this.eachComment(this.comments, (comment) => {
const notify = this.unread.find(o => o.comment_id === comment.data.id)
if (notify) {
comment.setUnread(true)
comment.setOpenURL(notify.read_link)
comment.openEvt = () => {
this.unread = this.unread.filter(o => o.comment_id !== comment.data.id) // remove
this.ctx.trigger('unread-update', {
notifies: this.unread
})
}
} else {
comment.setUnread(false)
}
})
}
}
/** 前端版本检测 */
public versionCheck(versionData: ApiVersionData): boolean {
const needVersion = versionData?.fe_min_version || '0.0.0'
const needUpdate = Utils.versionCompare(needVersion, ARTALK_VERSION) === 1
if (needUpdate) {
// 需要更新
const errEl = Utils.createElement(`<div>前端 Artalk 版本已过时,请更新以获得完整体验<br/>`
+ `若您是站点管理员,请前往 “<a href="https://artalk.js.org/" target="_blank">官方文档</a>” 获取帮助`
+ `<br/><br/>`
+ `<span style="color: var(--at-color-meta);">前端版本 ${ARTALK_VERSION},需求版本 >= ${needVersion}</span><br/><br/>`
+ `</div>`)
const ignoreBtn = Utils.createElement('<span style="cursor:pointer;">忽略</span>')
ignoreBtn.onclick = () => {
Ui.setError(this.ctx, null)
this.ctx.conf.versionCheck = false
this.fetchComments(0)
}
errEl.append(ignoreBtn)
Ui.setError(this.ctx, errEl, '<span class="atk-warn-title">Artalk Warn</span>')
}
return needUpdate
}
}
@@ -12,11 +12,13 @@ export default class List extends ListLite {
private $closeCommentBtn!: HTMLElement
private $openSidebarBtn!: HTMLElement
private $unreadBadge!: HTMLElement
private $commentCount!: HTMLElement
private $dropdownWrap?: HTMLElement
constructor (ctx: Context) {
const el = Utils.createElement(ListHTML)
super(ctx, el)
super(ctx)
// 把 listLite $el 变为子元素
el.querySelector('.atk-list-body')!.append(this.$el)
@@ -34,15 +36,21 @@ export default class List extends ListLite {
// 分页模式
this.pageMode = this.conf.pagination?.readMore ? 'read-more' : 'pagination'
this.pageSize = this.conf.pagination?.pageSize || 20
this.repositionAt = this.$el
// 操作按钮
this.initListActionBtn()
// 下拉选择层
this.$commentCount = this.$el.querySelector('.atk-comment-count')!
this.initDropdown()
// copyright
this.$el.querySelector<HTMLElement>('.atk-copyright')!.innerHTML = `Powered By <a href="https://artalk.js.org" target="_blank" title="Artalk v${ARTALK_VERSION}">Artalk</a>`
// event listen
this.ctx.on('list-reload', () => (this.reqComments(0))) // 刷新评论
this.ctx.on('list-reload', () => (this.fetchComments(0))) // 刷新评论
this.ctx.on('list-refresh-ui', () => (this.refreshUI()))
this.ctx.on('list-import', (data) => (this.importComments(data)))
this.ctx.on('list-insert', (data) => (this.insertComment(data)))
@@ -51,7 +59,7 @@ export default class List extends ListLite {
this.ctx.on('unread-update', (data) => (this.showUnreadBadge(data.notifies?.length || 0)))
}
private initListActionBtn () {
private initListActionBtn() {
// 侧边栏呼出按钮
this.$openSidebarBtn = this.$el.querySelector('[data-action="open-sidebar"]')!
this.$closeCommentBtn = this.$el.querySelector('[data-action="admin-close-comment"]')!
@@ -71,10 +79,10 @@ export default class List extends ListLite {
}
/** 刷新界面 */
public refreshUI () {
public refreshUI() {
super.refreshUI()
this.$el.querySelector<HTMLElement>('.atk-comment-count-num')!.innerText = String(this.commentsCount)
this.$el.querySelector<HTMLElement>('.atk-comment-count-num')!.innerText = String(Number(this.data?.total) || 0)
// 已输入个人信息
if (!!this.ctx.user.data.nick && !!this.ctx.user.data.email) {
@@ -85,7 +93,7 @@ export default class List extends ListLite {
// 仅管理员显示控制
this.ctx.trigger('check-admin-show-el')
this.$openSidebarBtn.innerText = (!this.ctx.user.data.isAdmin) ? '通知中心' : '控制中心'
this.$openSidebarBtn.querySelector<HTMLElement>('.atk-text')!.innerText = (!this.ctx.user.data.isAdmin) ? '通知中心' : '控制中心'
// 关闭评论
if (!!this.data && !!this.data.page && this.data.page.admin_only === true) {
@@ -97,7 +105,7 @@ export default class List extends ListLite {
}
}
public onLoad(data: ListData, offset: number) {
protected onLoad(data: ListData, offset: number) {
super.onLoad(data, offset)
// 检测锚点跳转
@@ -130,7 +138,7 @@ export default class List extends ListLite {
// TODO 若找不到评论的情况
// if (!comment) { // 若找不到评论
// if (this.hasMoreComments) { // 阅读更多,并重试
// await this.reqComments()
// await this.fetchComments()
// }
// }
@@ -161,7 +169,7 @@ export default class List extends ListLite {
})
}
public showUnreadBadge (count: number) {
public showUnreadBadge(count: number) {
if (count > 0) {
this.$unreadBadge.innerText = `${Number(count || 0)}`
this.$unreadBadge.style.display = 'block'
@@ -169,4 +177,58 @@ export default class List extends ListLite {
this.$unreadBadge.style.display = 'none'
}
}
/** 初始化选择下拉层 */
protected initDropdown() {
this.$dropdownWrap = this.$commentCount
this.$commentCount.classList.add('atk-dropdown-wrap')
// 插入图标
this.$commentsWrap.append(Utils.createElement(`<span class="atk-arrow-down-icon"></span>`))
const reloadUseParamsEditor = (func: (p: any) => void) => {
this.paramsEditor = (p) => { func(p) }
this.fetchComments(0)
}
// 下拉列表
const dropdownList = [
['最新', () => { reloadUseParamsEditor(p => { p.sort_by = 'date_desc' }) }],
['最热', () => { reloadUseParamsEditor(p => { p.sort_by = 'vote' }) }],
['最早', () => { reloadUseParamsEditor(p => { p.sort_by = 'date_asc' }) }],
['作者', () => { reloadUseParamsEditor(p => { p.view_only_admin = true }) }],
]
// 列表项点击事件
let curtActive = 0 // 当前选中
const onItemClick = (i: number, $item: HTMLElement, name: string, action: Function) => {
action()
// set active
curtActive = i
$dropdown.querySelectorAll('.active').forEach((e) => { e.classList.remove('active') })
$item.classList.add('active')
// 关闭层 (临时消失,取消 :hover)
$dropdown.style.display = 'none'
setTimeout(() => { $dropdown.style.display = '' }, 80)
}
// 生成列表元素
const $dropdown = Utils.createElement(`<ul class="atk-dropdown atk-fade-in"></ul>`)
dropdownList.forEach((item, i) => {
const name = item[0] as string
const action = item[1] as Function
const $item = Utils.createElement(`<li class="atk-dropdown-item"><span></span></li>`)
const $link = $item.querySelector<HTMLElement>('span')!
$link.innerText = name
$link.onclick = () => { onItemClick(i, $item, name, action) }
$dropdown.append($item)
if (i === curtActive) $item.classList.add('active') // 默认选中项
})
this.$dropdownWrap.append($dropdown)
}
}
@@ -4,7 +4,7 @@ import * as Ui from '../lib/ui'
export interface PaginationConf {
/** 每页条数 */
pageSize?: number
pageSize: number
/** 回调函数 */
onChange: (offset: number) => void
@@ -21,7 +21,7 @@ export default class Pagination {
public page: number = 1
get pageSize(): number {
return this.conf.pageSize || 15
return this.conf.pageSize
}
get offset(): number {
return this.pageSize * (this.page - 1)
@@ -2,7 +2,9 @@ import * as Utils from '../lib/utils'
export interface ReadMoreBtnConf {
/** 回调函数 */
onClick: () => void
onClick: (offset: number) => void
pageSize: number
}
/**
@@ -13,6 +15,13 @@ export default class ReadMoreBtn {
public $el: HTMLElement
private $loading: HTMLElement
private $text: HTMLElement
private offset: number = 0
private total: number = 0
/** 是否有更多内容 */
get hasMore() {
return this.total > (this.offset + this.conf.pageSize)
}
public constructor(conf: ReadMoreBtnConf) {
this.conf = conf
@@ -21,18 +30,19 @@ export default class ReadMoreBtn {
`<div class="atk-list-read-more" style="display: none;">
<div class="atk-list-read-more-inner">
<div class="atk-loading-icon" style="display: none;"></div>
<span class="atk-text">查看更多</span>
<span class="atk-text">加载更多</span>
</div>
</div>`)
this.$loading = this.$el.querySelector<HTMLElement>('.atk-loading-icon')!
this.$text = this.$el.querySelector<HTMLElement>('.atk-text')!
this.$el.onclick = () => this.click()
this.$el.onclick = () => { this.click() }
}
click() {
this.conf.onClick()
if (this.hasMore) this.conf.onClick(this.offset + this.conf.pageSize)
this.checkDisabled()
}
/** 显示 */
@@ -62,4 +72,17 @@ export default class ReadMoreBtn {
this.$el.classList.remove('atk-err')
}, 2000) // 2s后错误提示复原
}
/** 更新数据 */
public update(offset: number, total: number) {
this.offset = offset
this.total = total
this.checkDisabled()
}
public checkDisabled() {
if (this.hasMore) this.show()
else this.hide()
}
}
@@ -0,0 +1,143 @@
import '../style/sidebar-layer.less'
import Context from '@/context'
import Component from '@/lib/component'
import * as Utils from '@/lib/utils'
import SidebarHTML from './html/sidebar-layer.html?raw'
import * as Ui from '@/lib/ui'
import Layer from './layer'
export default class SidebarLayer extends Component {
public layer?: Layer
public $header: HTMLElement
public $closeBtn: HTMLElement
public $iframeWrap: HTMLElement
public $iframe?: HTMLIFrameElement
constructor(ctx: Context) {
super(ctx)
this.$el = Utils.createElement(SidebarHTML)
this.$header = this.$el.querySelector('.atk-sidebar-header')!
this.$closeBtn = this.$header.querySelector('.atk-sidebar-close')!
this.$iframeWrap = this.$el.querySelector('.atk-sidebar-iframe-wrap')!
this.$closeBtn.onclick = () => {
this.hide()
}
// event
this.ctx.on('sidebar-show', () => (this.show()))
this.ctx.on('sidebar-hide', () => (this.hide()))
this.ctx.on('user-changed', () => { this.firstShow = true })
}
private firstShow = true
/** 显示 */
public async show() {
this.$el.style.transform = '' // 动画清除,防止二次打开失效
// 获取 Layer
if (this.layer == null) {
this.layer = new Layer(this.ctx, 'sidebar', this.$el)
this.layer.afterHide = () => {
// 防止评论框被吞
if (this.ctx.conf.editorTravel === true) {
this.ctx.trigger('editor-travel-back')
}
}
}
this.layer.show()
// viewWrap 滚动条归位
// this.$viewWrap.scrollTo(0, 0)
// 执行动画
setTimeout(() => {
this.$el.style.transform = 'translate(0, 0)'
}, 20)
// 第一次加载
if (this.firstShow) {
this.$iframeWrap.innerHTML = ''
this.$iframe = Utils.createElement<HTMLIFrameElement>('<iframe></iframe>')
const baseURL = (import.meta.env.MODE === 'development') ? 'http://localhost:23367/'
: `${this.conf.server.replace(/\/$/, '')}/../sidebar/`
const userData = encodeURIComponent(JSON.stringify(this.ctx.user.data))
// 测试是否能访问
this.iframeLoad(`${baseURL}`
+ `?pageKey=${encodeURIComponent(this.conf.pageKey)}`
+ `&site=${encodeURIComponent(this.conf.site || '')}`
+ `&user=${userData}`
+ `${((this.conf.darkMode) ? `&darkMode=1` : ``)}`)
this.$iframeWrap.append(this.$iframe)
this.firstShow = false
} else {
// 暗黑模式
if (this.conf.darkMode && !this.$iframe!.src.match(/darkMode=1$/)) {
this.iframeLoad(`${this.$iframe!.src}&darkMode=1`)
}
if (!this.conf.darkMode && this.$iframe!.src.match(/darkMode=1$/)) {
this.iframeLoad(this.$iframe!.src.replace(/&darkMode=1$/, ''))
}
}
}
/** 隐藏 */
public hide() {
// 执行动画
this.$el.style.transform = ''
this.layer?.hide()
}
public iframeLoad(src: string) {
if (!this.$iframe) return
this.$iframe.src = src
// 加载动画
Ui.showLoading(this.$iframeWrap)
this.$iframe.onload = () => {
Ui.hideLoading(this.$iframeWrap)
}
// this.checkReqStatus(src) // 判不准,删了,没啥用
}
loadingTimer: number|null = null
// 测试可访问性 (由于 iframe 测不准,需要额外请求)
public async checkReqStatus(url: string) {
if (this.loadingTimer !== null) window.clearTimeout(this.loadingTimer)
this.loadingTimer = window.setTimeout(async () => {
try {
await fetch(url)
} catch (err) {
console.log(err)
// 请求失败,显示错误提示
const $errAlert = Utils.createElement(
`<div class="atk-err-alert">` +
` <div class="atk-title">侧边栏似乎打开失败</div>` +
` <div class="atk-text"><span id="AtkReload">重新加载</span> / <span id="AtkCancel">取消</span></div>` +
`</div>`
)
const $reloadBtn = $errAlert.querySelector<HTMLElement>('#AtkReload')!
const $cancelBtn = $errAlert.querySelector<HTMLElement>('#AtkCancel')!
$reloadBtn.onclick = () => {
this.iframeLoad(url)
$errAlert.remove()
}
$cancelBtn.onclick = () => { // 提供取消按钮,防止误判
$errAlert.remove()
}
this.$iframeWrap.append($errAlert)
}
}, 2000) // 2s 后开始检测
}
}
File renamed without changes.
@@ -17,6 +17,7 @@ const defaults: ArtalkConfig = {
vote: true,
voteDown: false,
uaBadge: true,
pvEl: '#ArtalkPV',
flatMode: 'auto',
maxNesting: 3,
gravatar: {
@@ -25,7 +26,7 @@ const defaults: ArtalkConfig = {
},
pagination: {
pageSize: 15,
pageSize: 20,
readMore: true,
autoLoad: true,
},
File renamed without changes.
File renamed without changes.
@@ -104,7 +104,9 @@ function Detect (userAgent) {
'6.0': 'Vista',
5.2: 'XP',
5.1: 'XP',
'5.0': '2000'
'5.0': '2000',
'10.0': '10',
'11.0': '11' // 自定的,不是微软官方的判断方法
}
return hash[v] || v
},
File renamed without changes.
File renamed without changes.
@@ -98,6 +98,7 @@ export function timeAgo(date: Date) {
/** 所有图片加载完毕后执行 */
export function onImagesLoaded($container: HTMLElement, event: Function) {
if (!$container) return
const images = $container.getElementsByTagName('img')
if (!images.length) return
let loaded = images.length
@@ -121,7 +122,7 @@ export function getGravatarURL(ctx: Context, emailMD5: string) {
}
export function sleep(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms));
return new Promise(resolve => { setTimeout(resolve, ms) });
}
/** 版本号比较(a < b :-1 | 0 | b < a :1) */
@@ -177,3 +178,28 @@ export function marked(ctx: Context, src: string): string {
return markedInstance.parse(src)
}
/** 获取修正后的 UserAgent */
export async function getCorrectUserAgent() {
const uaRaw = navigator.userAgent
const uaData = (navigator as any).userAgentData // @link https://web.dev/migrate-to-ua-ch/
if (!uaData || !uaData.getHighEntropyValues) {
return uaRaw
}
// microsoft f******k you!!!!!
// @link https://docs.microsoft.com/en-us/microsoft-edge/web-platform/how-to-detect-win11
let uaGot: any = null
try {
uaGot = await uaData.getHighEntropyValues(["platformVersion"])
} catch (err) { console.error(err); return uaRaw }
if (uaData.platform !== "Windows") { return uaRaw }
const majorPlatformVersion = Number(uaGot.platformVersion.split('.')[0])
if (majorPlatformVersion >= 13) {
// Win11 样本:"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.102 Safari/537.36"
return uaRaw.replace(/Windows\W+NT\W+10.0/, 'Windows NT 11.0')
}
return uaRaw
}
File renamed without changes.
+41
View File
@@ -0,0 +1,41 @@
.artalk, .atk-layer-wrap {
--at-color-font: #2a2e2e;
--at-color-deep: #2a2e2e;
--at-color-sub: #757575;
--at-color-grey: #999;
--at-color-meta: #757e91;
--at-color-border: #eceff2;
--at-color-light: #4285f4;
--at-color-bg: #FFF;
--at-color-bg-transl: rgba(255, 255, 255, 0.94);
--at-color-bg-grey: #F4F4F4;
--at-color-bg-grey-transl: rgba(244, 244, 244, 0.75);
--at-color-bg-light: rgba(29, 161, 242, 0.1);
--at-color-main: #0083ff;
--at-color-red: #ff5652;
--at-color-pink: #fa5a57;
--at-color-yellow: #ff7c37;
--at-color-green: #4caf50;
--at-color-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0), rgba(255, 255, 255));
&.atk-dark-mode {
--at-color-font: #fff;
--at-color-deep: #e7e7e7;
--at-color-sub: #e7e7e7;
--at-color-grey: #fff;
--at-color-meta: #fff;
--at-color-border: #2d3235;
--at-color-light: #687a86;
--at-color-bg: #1e2224;
--at-color-bg-transl: rgba(30, 34, 36, 0.95);
--at-color-bg-grey: #46494e;
--at-color-bg-grey-transl: rgba(8, 8, 8, 0.95);
--at-color-bg-light: rgba(29, 161, 242, 0.1);
--at-color-main: #0083ff;
--at-color-red: #ff5652;
--at-color-pink: #fa5a57;
--at-color-yellow: #ff7c37;
--at-color-green: #4caf50;
--at-color-gradient: linear-gradient(180deg, rgba(30, 34, 36, 0), rgba(30, 34, 36, 1));
}
}
+20
View File
@@ -0,0 +1,20 @@
.atk-slim-scrollbar() {
&::-webkit-scrollbar {
width: 4px;
height: 4px;
background: transparent;
}
&::-webkit-scrollbar-thumb,
&::-webkit-scrollbar-thumb:window-inactive {
background: rgba(86,86,86,.3);
}
&::-webkit-scrollbar-thumb:vertical:hover {
background: rgba(65,74,82,.77);
}
&::-webkit-scrollbar-thumb:vertical:active {
background: rgba(41,47,53,.77);
}
}
@@ -0,0 +1,4 @@
@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: 4px;
+301
View File
@@ -0,0 +1,301 @@
.atk-comment-wrap {
overflow: hidden;
position: relative;
border-bottom: 1px solid var(--at-color-border);
&.atk-flash-once {
animation: atkFlashOnce 1s ease-in-out 0s;
@keyframes atkFlashOnce {
from { background: rgba(0, 131, 255, 0.2); }
to { background: transparent; }
}
}
&.atk-unread:before {
content: ' ';
position: absolute;
left: 0;
top: 10%;
width: 3px;
height: 80%;
background: var(--at-color-main);
}
&.atk-openable {
cursor: pointer;
&:hover {
background: var(--at-color-bg-grey);
}
.atk-height-limit:after {
background: transparent !important;
}
}
&:last-child {
border-bottom: none;
}
}
.atk-comment {
display: block;
padding: 12px 10px;
& > .atk-avatar {
display: block;
padding: 2px 0;
float: left;
img {
width: 48px;
height: 48px;
border-radius: 3px;
}
}
& > .atk-main {
display: block;
margin-left: 62px;
& > .atk-header {
line-height: 1;
font-size: 13px;
margin-bottom: 3px;
overflow: hidden;
position: relative;
display: flex;
flex-wrap: wrap;
align-items: center;
.atk-item {
display: flex;
margin-bottom: 2px;
color: var(--at-color-meta);
&:not(:last-child) {
margin-right: 6px;
}
&.atk-nick, &.atk-nick a {
font-size: 14px;
color: var(--at-color-main);
text-decoration: none;
}
}
.badge() {
color: var(--at-color-meta);
background: var(--at-color-bg-grey);
padding: 0 6px;
line-height: 18px;
border-radius: 3px;
&:not(:last-child) {
margin-right: 6px;
}
}
.atk-badge {
.badge();
color: #fff;
}
.atk-pinned-badge {
.badge();
color: #fff;
background: #f44336;
}
.atk-ua {
.badge()
}
.atk-ua-wrap {
@media only screen and (max-width: 768px) {
display: block;
}
}
}
& > .atk-body {
display: block;
overflow: hidden;
position: relative;
img {
max-width: 100%;
}
& > .atk-content {
word-break: break-all;
&.atk-type-collapsed {
border: 3px solid var(--at-color-bg-grey);
border-bottom: 0;
padding: 5px 10px;
border-radius: @block-radius @block-radius 0 0;
margin-bottom: -5px;
}
& > *:first-child {
margin-top: 0;
}
& > *:last-child {
margin-bottom: 0;
}
.atk-height-limit-btn {
bottom: 5px;
}
}
& > .atk-pending {
color: var(--at-color-meta);
margin: 3px 0;
font-size: 13px;
padding: 10px 18px;
display: block;
background: var(--at-color-bg-grey);
border-left: 4px solid #f44336;
}
& > .atk-reply-to {
padding: 5px 15px;
border-left: 3px solid #eee;
margin-bottom: 10px;
position: relative;
.atk-meta {
font-size: 15px;
.atk-nick {
color: var(--at-color-main)
}
}
.atk-content {
margin-top: 5px;
}
}
& > .atk-collapsed {
margin: 3px 0;
font-size: 13px;
padding: 10px 18px;
display: block;
background: var(--at-color-bg-grey);
border-radius: @block-radius;
.atk-text {
color: var(--at-color-meta);
}
.atk-show-btn {
color: var(--at-color-main);
cursor: pointer;
user-select: none;
margin-left: 3px;
&:hover {
color: var(--at-color-main);
}
}
}
}
& > .atk-footer {
margin-top: 12px;
.atk-actions {
display: flex;
flex-direction: row;
align-items: center;
& > span {
color: var(--at-color-meta);
font-size: 13px;
line-height: 15px;
display: inline-flex;
cursor: pointer;
user-select: none;
&.atk-error, &.atk-error:hover {
color: var(--at-color-red);
}
&:not(:first-child):not(.atk-hide) {
margin-left: 8px;
padding-left: 8px;
border-left: 1px solid var(--at-color-border);
}
&:hover {
color: var(--at-color-deep);
}
}
}
}
}
/** 内容限高 **/
.atk-height-limit:after {
position: absolute;
z-index: 1;
display: block;
overflow: hidden;
width: 100%;
content: " ";
bottom: 0;
height: 80px;
background: var(--at-color-gradient);
}
.atk-height-limit-btn {
z-index: 10;
position: absolute;
left: 50%;
bottom: 10px;
transform: translateX(-50%);
cursor: pointer;
border: 1px solid var(--at-color-border);
border-radius: @block-radius;
background: var(--at-color-bg);
padding: 1px 20px;
font-size: 15px;
color: var(--at-color-meta);
user-select: none;
}
// 避免重复显示按钮
.atk-height-limit .atk-height-limit .atk-height-limit-btn {
display: none;
}
}
/** 子评论区域 **/
.atk-comment-children {
& > .atk-comment-wrap {
margin-top: 10px;
border-left: 1px dashed var(--at-color-border);
border-bottom-color: transparent;
&:not(:last-child) {
margin-bottom: 5px;
}
& > .atk-comment {
padding-top: 4px;
padding-bottom: 4px;
& > .atk-avatar img {
width: 36px;
height: 36px;
}
& > .atk-main {
margin-left: 47px;
}
}
}
}
@@ -1,36 +1,32 @@
.atk-main-editor {
border: 1px solid @color-border;
border-radius: @block-radius;
margin-bottom: @block-margin;
overflow: hidden;
position: relative;
background: @color-bg;
@editor-padding: 10px;
z-index: 5;
position: relative;
overflow: hidden;
background: var(--at-color-bg);
border: 1px solid var(--at-color-border);
border-radius: @block-radius;
margin-bottom: 15px;
@media only screen and (max-width: 768px) {
& > {
margin-bottom: @block-margin-mobile;
}
margin-bottom: 7px;
}
& > .atk-editor-header {
& > .atk-header {
display: flex;
flex-direction: row;
padding: 0 @block-padding;
padding-top: calc(@block-padding / 2);
padding-bottom: 0;
padding: calc(@editor-padding / 2) @editor-padding 0;
input {
font-size: 14px;
border: 2px solid transparent;
transition: color .3s ease;
flex: 1;
width: 100%;
font-size: 14px;
background: transparent;
border: 2px solid transparent;
border-radius: calc(@block-radius / 2);
padding: 6px 5px;
resize: none;
outline: none;
width: 100%;
&:not(:last-child) {
margin-right: 2px;
@@ -38,59 +34,46 @@
}
}
& > .atk-editor-textarea-wrap {
.atk-editor-textarea {
font-size: 15px;
color: @color-font;
background-color: @color-bg;
margin-top: 2px;
padding: @block-padding calc(10px + 5px);
& > .atk-textarea-wrap {
position: relative;
& > .atk-textarea {
display: block;
width: 100%;
min-height: 130px;
resize: none;
word-wrap: break-word;
overflow: hidden;
color: var(--at-color-font);
font-size: 15px;
background-color: var(--at-color-bg);
border: 2px solid transparent;
border-radius: calc(@block-radius / 2);
width: 100%;
min-height: 130px;
margin-top: 2px;
padding: @editor-padding calc(10px + 5px);
resize: none;
word-wrap: break-word;
outline: none;
overflow: hidden;
}
.atk-close-comment {
display: flex;
user-select: none;
// background: repeating-linear-gradient(145deg, #f2f2f2, #f2f2f2 15px, #fff 0, #fff 30px);
padding: 0;
position: relative;
text-align: center;
min-height: 30px;
border-top: 1px solid @color-border;
margin-top: 4px;
font-weight: 100;
& > .atk-comment-closed {
pointer-events: none;
color: var(--at-color-meta);
font-size: 12px;
place-items: center;
justify-content: center;
place-content: flex-end;
flex-direction: row;
background: @color-bg;
& > span {
color: @color-meta;
border-radius: 2px;
margin-right: 14px;
}
background-color: var(--at-color-bg);
border-top: 1px solid var(--at-color-border);
padding: 5px 15px;
// background: repeating-linear-gradient(145deg, #f2f2f2, #f2f2f2 15px, #fff 0, #fff 30px);
}
.atk-send-reply {
& > .atk-send-reply {
z-index: 2;
position: absolute;
right: 10px;
bottom: 40px;
bottom: 8px;
max-width: calc(100% - 20px);
height: 30px;
line-height: 30px;
font-size: 14px;
background: @color-bg-grey-transl;
background: var(--at-color-bg-grey-transl);
padding: 0 30px 0 10px;
overflow: hidden;
text-overflow: ellipsis;
@@ -111,97 +94,89 @@
font-weight: 700;
&:hover {
color: @color-deep;
color: var(--at-color-deep);
}
}
}
}
& > .atk-editor-plug-wrap {
& > .atk-plug-wrap {
position: relative;
height: 180px;
width: 100%;
overflow: hidden;
border-top: 1px solid @color-border;
animation: .3s both atkFadeIn;
transition: .2s height ease-in-out;
border-top: 1px solid var(--at-color-border);
animation: 0.3s both atkFadeIn;
transition: 0.2s height ease-in-out;
}
& > .atk-editor-bottom {
& > .atk-bottom {
display: flex;
flex-direction: row;
border-top: 1px solid @color-border;
padding: 0 0 0 calc(@block-padding - 5px);
justify-content: space-between;
border-top: 1px solid var(--at-color-border);
padding: 0 0 0 calc(@editor-padding - 5px);
.atk-editor-bottom-part {
& > .atk-item {
display: flex;
flex: 1;
flex-direction: row;
align-items: center;
&.atk-left {
flex: inherit;
}
&.atk-right {
justify-content: flex-end;
}
}
.atk-editor-action {
.atk-plug-btn {
padding: 0 8px;
cursor: pointer;
color: @color-grey;
color: var(--at-color-grey);
font-size: @font-size;
user-select: none;
& > i {
color: @color-grey;
&:not(:last-child) {
border-right: 1px solid var(--at-color-border);
}
&.active {
color: var(--at-color-main);
svg.markdown path {
fill: var(--at-color-main);
}
}
i {
color: var(--at-color-grey);
display: inline-flex;
vertical-align: sub;
margin-left: 4px;
svg.markdown path {
fill: @color-grey;
fill: var(--at-color-grey);
}
}
&.active {
color: @color-main;
& > i > svg.markdown path {
fill: @color-main;
}
}
&:not(:last-child) {
border-right: 1px solid @color-border;
}
}
.atk-send-btn {
background: @color-main;
color: #FFF;
background: var(--at-color-main);
color: #fff;
font-size: @font-size;
border: none;
padding: 0 30px;
margin: 0;
height: 30px;
cursor: pointer;
transition: opacity .3s ease-in-out;
transition: opacity 0.3s ease-in-out;
outline: none;
font-size: @font-size;
margin: 0;
&:active {
opacity: .9;
opacity: 0.9;
}
}
}
& > .atk-editor-notify-wrap {
& > .atk-notify-wrap {
z-index: 9;
position: absolute;
right: -2px;
bottom: 36px;
width: 225px;
z-index: 9;
opacity: .83;
opacity: 0.83;
}
}
@@ -5,7 +5,73 @@
display: flex;
flex-direction: row;
padding: 10px 10px 5px 10px;
border-bottom: 1px solid @color-border;
border-bottom: 1px solid var(--at-color-border);
.atk-text {
display: inline-block;
}
/* 下拉选择层 */
.atk-dropdown-wrap {
position: relative;
cursor: pointer;
.atk-arrow-down-icon {
cursor: pointer;
vertical-align: middle;
border-top: 6px solid var(--at-color-grey);
border-left: 4px solid transparent;
border-right: 4px solid transparent;
margin-top: -1px;
margin-left: 0.4rem;
display: inline-block;
}
&:hover {
.atk-dropdown {
display: block;
}
}
.atk-dropdown {
z-index: 888;
display: none;
height: auto !important;
max-height: calc(100vh - 2.7rem);
overflow-y: auto;
position: absolute;
top: 100%;
right: 0;
width: 100%;
background-color: var(--at-color-bg);
padding: 0.6rem 0;
border: 1px solid var(--at-color-border);
text-align: center;
border-radius: 0.25rem;
white-space: nowrap;
margin: 0;
list-style: none;
}
.atk-dropdown-item {
span, a {
display: block;
line-height: 2rem;
position: relative;
border-bottom: none;
font-weight: 400;
padding: 0 1.5rem;
&:hover {
color: var(--at-color-main);
}
}
&.active span, a {
color: var(--at-color-main);
}
}
}
.atk-comment-count {
font-size: 15px;
@@ -25,19 +91,19 @@
& > span {
font-size: 14px;
color: @color-meta;
color: var(--at-color-meta);
cursor: pointer;
user-select: none;
position: relative;
&.atk-on, &.atk-on * {
color: @color-main;
color: var(--at-color-main);
}
&:not(:last-child):not(.atk-hide) {
margin-right: 10px;
padding-right: 10px;
border-right: 1px solid @color-border;
border-right: 1px solid var(--at-color-border);
}
.atk-unread-badge {
@@ -45,7 +111,7 @@
top: -5px;
left: -6px;
color: #fff;
background: @color-pink;
background: var(--at-color-pink);
text-align: center;
min-width: 16px;
height: 16px;
@@ -80,12 +146,12 @@
.atk-copyright {
display: block;
font-size: 12px;
color: @color-meta;
color: var(--at-color-meta);
padding-right: 15px;
margin-top: 5px;
a {
color: @color-main;
color: var(--at-color-main);
text-decoration: none;
}
}
@@ -101,7 +167,7 @@
}
.atk-list-read-more {
border-top: 1px dashed @color-border;
border-top: 1px dashed var(--at-color-border);
margin-top: 20px;
padding-bottom: 20px;
@@ -124,11 +190,11 @@
width: 120px;
margin: 0 auto;
margin-top: -15px;
background: @color-bg;
border-color: @color-border;
background: var(--at-color-bg);
border-color: var(--at-color-border);
&.atk-err > .atk-text {
color: @color-red !important;
color: var(--at-color-red) !important;
}
& > .atk-loading-icon {
@@ -137,12 +203,12 @@
}
& > .atk-text {
color: @color-meta;
color: var(--at-color-meta);
}
&:hover {
& > .atk-text {
color: @color-main;
color: var(--at-color-main);
}
}
}
@@ -10,7 +10,7 @@
* {
box-sizing: border-box;
line-height: @line-height;
color: @color-font;
color: var(--at-color-font);
font-family: @font-family;
word-wrap: break-word;
word-break: break-all;
@@ -19,16 +19,20 @@
pre {
padding: 10px;
line-height: @line-height;
background-color: @color-bg-grey;
color: @color-font;
padding: 5px 10px;
border-radius: calc(@block-radius / 2);
background-color: var(--at-color-bg-grey);
color: var(--at-color-font);
padding: 10px 15px;
border-radius: @block-radius;
margin: 0;
font-family: Source Code Pro,courier new,Input Mono,PT Mono,SFMono-Regular,Consolas,Monaco,Menlo,PingFang SC,Liberation Mono,Microsoft YaHei,Courier,monospace;
code {
white-space: pre-wrap;
}
}
a {
color: @color-main;
color: var(--at-color-main);
text-decoration: none;
}
@@ -36,8 +40,8 @@
position: static;
margin: 10px 0 10px 0;
padding: 10px 20px 10px 20px;
background: @color-bg-grey;
border-left: 4px solid @color-quote;
background: var(--at-color-bg-grey);
border-left: 4px solid #687a86;
}
p:first-child {
@@ -48,10 +52,6 @@
margin-bottom: 0;
}
* {
box-sizing: border-box;
}
img {
max-width: 100%;
}
@@ -67,7 +67,7 @@
position: absolute;
top: 0;
left: 0;
background: @color-bg;
background: var(--at-color-bg);
z-index: 10;
align-items: center;
justify-content: center;
@@ -170,7 +170,7 @@
i.atk-icon {
width: 20px;
height: 20px;
background-color: @color-deep;
background-color: var(--at-color-deep);
background-size: contain;
background-repeat: no-repeat;
mask-repeat: no-repeat;
@@ -182,7 +182,7 @@ i.atk-icon {
}
.atk-icon-del {
background-color: @color-red !important;
background-color: var(--at-color-red) !important;
mask-image: url("data:image/svg+xml,%3Csvg width='22' height='22' viewBox='0 0 22 22' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M2.29167 5.04166L4.81251 5.04166M4.81251 5.04166L4.81251 18.3333C4.81251 18.5865 5.01771 18.7917 5.27084 18.7917L16.7292 18.7917C16.9823 18.7917 17.1875 18.5865 17.1875 18.3333V5.04166M4.81251 5.04166L7.33334 5.04166M17.1875 5.04166L19.7083 5.04166M17.1875 5.04166L14.6667 5.04166M7.33334 5.04166V3.20833L14.6667 3.20833V5.04166M7.33334 5.04166L14.6667 5.04166' stroke='%23D06565' stroke-width='2'/%3E%3Cpath d='M9.16667 8.25V15.125M12.8333 8.25V15.125' stroke='%23D06565' stroke-width='2'/%3E%3C/svg%3E");
}
@@ -205,14 +205,14 @@ i.atk-icon {
/* Layer */
.atk-error-layer {
&:extend(.atk-full-layer);
background-color: @color-bg-transl;
background-color: var(--at-color-bg-transl);
.atk-error-title {
color: @color-red;
color: var(--at-color-red);
}
.atk-warn-title {
color: @color-yellow;
color: var(--at-color-yellow);
}
.atk-error-title, .atk-warn-title {
@@ -228,11 +228,11 @@ i.atk-icon {
padding: 0 20px;
* {
color: @color-deep;
color: var(--at-color-deep);
}
a {
color: @color-meta;
color: var(--at-color-meta);
}
}
}
@@ -240,7 +240,7 @@ i.atk-icon {
.atk-layer-dialog-wrap {
@formInputHeight: 30px;
&:extend(.atk-full-layer);
background-color: @color-bg-transl;
background-color: var(--at-color-bg-transl);
& > .atk-layer-dialog {
width: 25%;
@@ -248,19 +248,19 @@ i.atk-icon {
& > .atk-layer-dialog-content {
.atk-captcha-img {
cursor: pointer;
width: 100px;
width: 170px;
height: auto;
margin-right: 10px;
padding-right: 10px;
border-right: 1px solid @color-border;
border-right: 1px solid var(--at-color-border);
vertical-align: bottom;
}
input {
width: 100%;
line-height: calc(@formInputHeight + 4px);
background-color: @color-bg;
border: 1px solid @color-border;
background-color: var(--at-color-bg);
border: 1px solid var(--at-color-border);
border-radius: calc(@block-radius / 2);
outline: none;
padding: 0 6px;
@@ -279,9 +279,9 @@ i.atk-icon {
flex: 1;
display: block;
cursor: pointer;
border: 1px solid @color-main;
border: 1px solid var(--at-color-main);
background: transparent;
color: @color-main;
color: var(--at-color-main);
border-radius: calc(@block-radius / 2);
padding: 0 15px;
line-height: @formInputHeight;
@@ -289,7 +289,7 @@ i.atk-icon {
&:active {
color: #FFF;
background: @color-main;
background: var(--at-color-main);
}
&:not(:last-child) {
@@ -355,19 +355,19 @@ i.atk-icon {
/* Common Action Btn */
.atk-common-action-btn {
&.atk-btn-confirm {
color: @color-yellow !important;
color: var(--at-color-yellow) !important;
}
&.atk-btn-warn {
color: @color-yellow !important;
color: var(--at-color-yellow) !important;
}
&.atk-btn-error {
color: @color-red !important;
color: var(--at-color-red) !important;
}
&.atk-btn-success {
color: @color-green !important;
color: var(--at-color-green) !important;
}
}
@@ -9,11 +9,11 @@
& > .atk-input {
font-size: 15px;
height: 32px;
border: 1px solid @color-border;
border: 1px solid var(--at-color-border);
border-radius: 3px;
padding: 0 5px;
text-align: center;
background: @color-bg;
background: var(--at-color-bg);
}
& > .atk-btn {
@@ -25,11 +25,11 @@
align-items: center;
&:hover {
background: @color-bg-grey;
background: var(--at-color-bg-grey);
}
&.atk-disabled {
color: @color-sub;
color: var(--at-color-sub);
&:hover {
cursor: default;
@@ -40,13 +40,13 @@
& > .atk-input {
background: transparent;
color: @color-font;
color: var(--at-color-font);
font-size: 18px;
width: 60px;
outline: none;
&:focus {
border-color: @color-main;
border-color: var(--at-color-main);
}
}
@@ -0,0 +1,92 @@
@header-height: 60px;
@nav-height: 40px;
.atk-sidebar-layer {
@width: 430px;
position: fixed;
z-index: 99999;
top: 0;
right: 0;
width: @width;
height: 100%;
background: var(--at-color-bg);
transition: transform 450ms cubic-bezier(0.23, 1, 0.32, 1) 0ms;
transform: translate(@width, 0px);
@media only screen and (max-width: @width) {
& {
width: 100%;
}
}
.atk-sidebar-inner {
position: relative;
height: 100%;
}
// Header
.atk-sidebar-header {
position: absolute;
top: 0;
right: 0;
display: flex;
flex-direction: row;
align-items: center;
z-index: 99999;
.atk-sidebar-close {
display: flex;
flex-direction: column;
width: @header-height;
height: @header-height;
align-items: center;
place-content: center;
cursor: pointer;
user-select: none;
margin-left: 10px;
&:hover .atk-icon-close {
background-color: rgba(232, 17, 35, 0.9);
}
}
}
.atk-sidebar-iframe-wrap {
height: 100%;
position: relative;
iframe {
border: 0;
width: 100%;
height: 100%;
}
.atk-err-alert {
z-index: 9999;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
position: absolute;
background: var(--at-color-bg);
padding: 40px 30px;
width: 80%;
text-align: center;
border-radius: 4px;
.atk-title {
font-size: 1.4em;
margin-bottom: 20px;
color: var(--at-color-font);
}
.atk-text {
color: var(--at-color-font);
span {
cursor: pointer;
color: var(--at-color-main);
}
}
}
}
}
+27
View File
@@ -0,0 +1,27 @@
{
"compilerOptions": {
"baseUrl": ".",
"outDir": "dist",
"target": "esnext",
"useDefineForClassFields": true,
"module": "esnext",
"lib": ["esnext", "dom"],
"moduleResolution": "node",
"strict": true,
"sourceMap": true,
"experimentalDecorators": true,
"resolveJsonModule": true,
"esModuleInterop": true,
"noEmit": true,
"noImplicitAny": false,
"allowSyntheticDefaultImports": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"paths": {
"~/*": ["./*"],
"@/*": ["./src/*"]
}
},
"include": ["./src", "./types"]
}
@@ -48,7 +48,7 @@ export default interface ArtalkConfig {
}
/** 内容限高 */
heightLimit: {
heightLimit?: {
/** 评论内容限高 */
content?: number
@@ -62,6 +62,9 @@ export default interface ArtalkConfig {
/** 评论投票反对按钮 */
voteDown?: boolean
/** PV 元素 Selector */
pvEl?: string
/** 暗黑模式 */
darkMode?: boolean
@@ -29,6 +29,9 @@ export interface CommentData {
/** 是否待审 */
is_pending?: boolean
/** 是否置顶 */
is_pinned?: boolean
/** 是否为管理员 */
is_admin?: boolean
@@ -64,8 +67,8 @@ export interface ListData {
/** 评论数据 */
comments: CommentData[]
/** 父级评论总数 */
total_parents: number
/** 根评论总数 */
total_roots: number
/** 评论总数(包括所有子评论) */
total: number
File renamed without changes.
File renamed without changes.
File renamed without changes.
+1
View File
@@ -0,0 +1 @@
/// <reference types="vite/client" />
File renamed without changes.
File renamed without changes.
File renamed without changes.
-19
View File
@@ -1,19 +0,0 @@
<div class="atk-main-editor">
<div class="atk-editor-header">
<input name="nick" placeholder="昵称" class="atk-nick" type="text" required="required">
<input name="email" placeholder="邮箱" class="atk-email" type="email" required="required">
<input name="link" placeholder="网址 (https://)" class="atk-link" type="url">
</div>
<div class="atk-editor-textarea-wrap">
<div class="atk-close-comment" style="display: none;"><span>仅管理员可评论</span></div>
<textarea id="atk-editor-textarea" class="atk-editor-textarea" placeholder=""></textarea>
</div>
<div class="atk-editor-plug-wrap" style="display: none;"></div>
<div class="atk-editor-bottom">
<div class="atk-editor-bottom-part atk-left atk-editor-plug-switcher-wrap"></div>
<div class="atk-editor-bottom-part atk-right">
<button type="button" class="atk-send-btn"></button>
</div>
</div>
<div class="atk-editor-notify-wrap"></div>
</div>
-588
View File
@@ -1,588 +0,0 @@
import Context from '../context'
import Component from '../lib/component'
import * as Utils from '../lib/utils'
import * as Ui from '../lib/ui'
import Api from '../api'
import Comment from './comment'
import { ListData, CommentData, NotifyData, ApiVersionData } from '~/types/artalk-data'
import Pagination from './pagination'
import ReadMoreBtn from './read-more-btn'
export default class ListLite extends Component {
private $parent: HTMLElement
private $commentsWrap: HTMLElement
public comments: Comment[] = []
public data?: ListData
private pageSize: number = 15 // 每次请求获取量
private offset: number = 0
public type?: 'all'|'mentions'|'mine'|'pending'
public noCommentText: string
public renderComment?: (comment: Comment) => void
public paramsEditor?: (params: any) => void
public onAfterLoad?: (data: ListData) => void
private isLoading: boolean = false
public isFirstLoad = true
public flatMode?: boolean
public pageMode: 'pagination'|'read-more' = 'pagination'
public pagination?: Pagination
public readMoreBtn?: ReadMoreBtn
public autoLoadScrollEvent?: any
public autoLoadListenerAt?: HTMLElement
public unread: NotifyData[] = []
public unreadHighlight = false
constructor (ctx: Context, $parent: HTMLElement) {
super(ctx)
this.$parent = $parent
this.$el = Utils.createElement(
`<div class="atk-list-lite">
<div class="atk-list-comments-wrap"></div>
</div>`)
this.$commentsWrap = this.$el.querySelector('.atk-list-comments-wrap')!
// 查看更多
this.pageSize = this.conf.pagination ? (this.conf.pagination.pageSize || this.pageSize) : this.pageSize
this.noCommentText = this.conf.noComment || '无评论'
// 评论时间自动更新
window.setInterval(() => {
this.$el.querySelectorAll<HTMLElement>('[data-atk-comment-date]').forEach(el => {
const date = el.getAttribute('data-atk-comment-date')
el.innerText = Utils.timeAgo(new Date(Number(date)))
})
}, 30 * 1000) // 30s 更新一次
this.ctx.on('unread-update', (data) => (this.updateUnread(data.notifies)))
}
public async reqComments(offset: number = 0) {
if (offset === 0 && this.pageMode !== 'pagination') { this.clearAllComments() }
// set loading
const showLoading = () => {
this.isLoading = true
if (offset === 0) Ui.showLoading(this.$el)
else if (this.pageMode === 'read-more') this.readMoreBtn!.setLoading(true)
else if (this.pageMode === 'pagination') this.pagination!.setLoading(true)
}
const hideLoading = () => {
this.isLoading = false
if (offset === 0) Ui.hideLoading(this.$el)
else if (this.pageMode === 'read-more') this.readMoreBtn!.setLoading(false)
else if (this.pageMode === 'pagination') this.pagination!.setLoading(false)
}
showLoading()
this.ctx.trigger('comments-load')
let listData: ListData
try {
listData = await new Api(this.ctx).get(offset, this.type, this.flatMode, this.paramsEditor)
} catch (e: any) {
this.onError(e.msg || String(e))
throw e
} finally {
hideLoading()
}
// version check
if (this.ctx.conf.versionCheck) {
const needUpdate = this.apiVersionCheck(listData.api_version || {})
if (needUpdate) return
}
// load data
this.offset = offset
try {
this.onLoad(listData, offset)
if (this.onAfterLoad) {
this.onAfterLoad(listData)
}
} catch (e: any) {
this.onError(String(e))
throw e
} finally {
hideLoading()
}
}
public onLoad(data: ListData, offset: number) {
Ui.setError(this.$el, null)
if (this.pageMode === 'pagination') {
this.clearAllComments()
}
this.data = data
this.importComments(data.comments)
// onLoad 时初始化
if (this.isFirstLoad) {
this.onLoadInit()
}
if (this.pageMode === 'pagination') {
this.pagination!.update(offset, this.data?.total_parents || 0)
}
if (this.pageMode === 'read-more') {
if (this.hasMoreComments) this.readMoreBtn!.show()
else this.readMoreBtn!.hide()
}
this.ctx.trigger('unread-update', { notifies: data.unread || [] })
this.isFirstLoad = false
}
/** 初始化加载更多 */
onLoadInit() {
if (this.autoLoadScrollEvent) {
const at = this.autoLoadListenerAt || document
at.removeEventListener('scroll', this.autoLoadScrollEvent)
}
if (this.pageMode === 'read-more') {
// 阅读更多按钮
const readMoreBtn = new ReadMoreBtn({
onClick: async () => {
const offset = this.offset + this.pageSize
await this.reqComments(offset)
},
})
if (this.readMoreBtn) this.readMoreBtn.$el.replaceWith(readMoreBtn.$el)
else this.$el.append(readMoreBtn.$el)
this.readMoreBtn = readMoreBtn
// 滚动到底部自动加载
if (this.conf.pagination?.autoLoad) {
this.autoLoadScrollEvent = () => {
if (this.pageMode !== 'read-more') return
if (!this.hasMoreComments) return
if (this.isLoading) return
const $target = this.$el.querySelector<HTMLElement>('.atk-list-comments-wrap > .atk-comment-wrap:nth-last-child(3)') // 获取倒数第3个评论元素
if (!$target) return
if (Ui.isVisible($target, this.autoLoadListenerAt)) {
// 加载更多
this.readMoreBtn!.click()
}
}
const at = this.autoLoadListenerAt || document
at.addEventListener('scroll', this.autoLoadScrollEvent)
}
} else if (this.pageMode === 'pagination') {
// 分页条
const pagination = new Pagination(this.parentCommentsCount, {
pageSize: this.pageSize,
onChange: async (offset) => {
if (this.ctx.conf.editorTravel === true) {
this.ctx.trigger('editor-travel-back') // 防止评论框被吞
}
await this.reqComments(offset)
// 滚动到第一个评论的位置
if (this.$parent) {
let topPos = 0
if (!this.autoLoadListenerAt && this.$parent) {
topPos = Utils.getOffset(this.$parent).top
}
const at = this.autoLoadListenerAt || window
at.scroll({
top: topPos,
left: 0,
})
}
}
})
if (this.pagination) this.pagination.$el.replaceWith(pagination.$el)
else this.$el.append(pagination.$el)
this.pagination = pagination
}
}
public onError(msg: any) {
msg = String(msg)
console.error(msg)
if (this.isFirstLoad || this.pageMode === 'pagination') {
const errEl = Utils.createElement(`<span>${msg},无法获取评论列表数据<br/></span>`)
const retryBtn = Utils.createElement('<span style="cursor:pointer;">点击重新获取</span>')
retryBtn.onclick = () => {
this.reqComments(this.offset)
}
errEl.appendChild(retryBtn)
const adminBtn = Utils.createElement('<span atk-only-admin-show> | <span style="cursor:pointer;">打开控制台</span></span>')
adminBtn.onclick = () => {
this.ctx.trigger('sidebar-show')
}
if (!this.ctx.user.data.isAdmin) {
adminBtn.classList.add('atk-hide')
}
errEl.appendChild(adminBtn)
Ui.setError(this.$el, errEl)
} else {
this.readMoreBtn?.showErr(`获取失败`)
}
}
/** 刷新界面 */
public refreshUI() {
// 评论为空界面
const noComment = this.comments.length <= 0
let noCommentEl = this.$commentsWrap.querySelector<HTMLElement>('.atk-list-no-comment')
if (noComment) {
if (!noCommentEl) {
noCommentEl = Utils.createElement('<div class="atk-list-no-comment"></div>')
this.$commentsWrap.appendChild(noCommentEl)
noCommentEl.innerHTML = this.noCommentText
}
}
if (!noComment && noCommentEl)
noCommentEl.remove()
// 仅管理员显示控制
this.ctx.trigger('check-admin-show-el')
}
/** 创建新评论 */
public createComment(data: CommentData) {
const comment = new Comment(this.ctx, data)
comment.afterRender = () => {
if (this.renderComment) this.renderComment(comment)
}
comment.onDelete = (c) => {
this.deleteComment(c)
this.refreshUI()
}
return comment
}
/** 导入评论 · 通过请求数据 */
public importComments(rawData: CommentData[]) {
// 查找并导入所有子评论
const queryImportChildren = (parentC: Comment) => {
const children = rawData.filter(o => o.rid === parentC.data.id)
if (children.length === 0) return
children.forEach((itemData: CommentData) => {
itemData.is_allow_reply = parentC.data.is_allow_reply
const childC = this.createComment(itemData)
childC.render()
parentC.putChild(childC)
queryImportChildren(childC) // 递归
})
}
// 开始处理 rawData
if (!this.flatMode) {
rawData.filter((o) => o.rid === 0).forEach((rootCommentData: CommentData) => {
if (rootCommentData.is_collapsed) rootCommentData.is_allow_reply = false
const rootComment = this.createComment(rootCommentData)
rootComment.render()
this.comments.push(rootComment) // 将评论导入 comments 总表中
this.$commentsWrap.appendChild(rootComment.getEl())
rootComment.playFadeInAnim()
queryImportChildren(rootComment)
})
} else {
// 平铺模式
rawData.forEach((commentData: CommentData) => {
this.putCommentFlatMode(commentData, rawData, 'append')
})
}
this.eachComment(this.comments, (c) => {
if (c.getIsRoot()) this.checkMoreHide(c)
})
this.refreshUI()
this.ctx.trigger('comments-loaded')
}
/** 导入评论 · 平铺模式 */
private putCommentFlatMode(commentItem: CommentData, comments: CommentData[], insertMode: 'append'|'prepend') {
if (commentItem.is_collapsed) commentItem.is_allow_reply = false
const comment = this.createComment(commentItem)
if (commentItem.rid !== 0) {
const rComment = comments.find(o => o.id === commentItem.rid)
if (rComment) comment.replyTo = rComment
}
comment.render()
// 将评论导入 comments 总表中
if (insertMode === 'append') {
this.comments.push(comment)
} else {
this.comments.unshift(comment)
}
if (commentItem.visible) {
if (insertMode === 'append') {
this.$commentsWrap.appendChild(comment.getEl())
} else {
this.$commentsWrap.prepend(comment.getEl())
}
comment.playFadeInAnim()
}
this.checkMoreHide(comment)
return comment
}
/** 插入评论 · 首部添加 */
public insertComment(commentData: CommentData) {
if (!this.flatMode) {
const comment = this.createComment(commentData)
comment.render()
if (commentData.rid === 0) {
this.$commentsWrap.prepend(comment.getEl())
this.comments.unshift(comment)
this.checkMoreHide(comment)
} else {
// 子评论
const parent = this.findComment(commentData.rid)
if (parent) {
parent.putChild(comment)
if (parent.$children) this.removeHideMore(parent.$children)
this.eachComment(parent.children, (c) => {
this.checkMoreHide(c)
})
}
}
Ui.scrollIntoView(comment.getEl()) // 滚动到可以见
comment.playFadeInAnim() // 播放评论渐出动画
} else {
const comment = this.putCommentFlatMode(commentData, this.comments.map(c => c.data), 'prepend')
Ui.scrollIntoView(comment.getEl()) // 滚动到可以见
}
if (this.data) this.data.total += 1 // 评论数增加 1
this.refreshUI() // 更新 list 界面
this.ctx.trigger('comments-loaded')
}
checkMoreHide(c: Comment) {
const check = () => {
// 子评论内容过多隐藏
this.checkMoreHideEl(c, 'children')
// 评论内容过多隐藏
this.checkMoreHideEl(c, 'content')
// 平铺模式回复内容
if (c.$replyTo) this.checkMoreHideEl(c, 'replyTo')
}
// 执行检测
check()
// 图片加载完后再检测一次
if (c.$content.querySelectorAll('img').length) {
Utils.onImagesLoaded(c.$content, () => {
check()
})
}
}
/** 内容过多,限高显示 */
public checkMoreHideEl(comment: Comment, area: 'children'|'content'|'replyTo', allowHeight = 300) {
const childrenH = this.ctx.conf.heightLimit?.children
const contentH = this.ctx.conf.heightLimit?.content
if (area === 'children' && (!childrenH)) return
if ((area === 'content' || area === 'replyTo') && (!contentH)) return
// 限高
if (area === 'children') allowHeight = childrenH || 300
else allowHeight = contentH || 200
// target 准备
let $target!: HTMLElement
if (area === 'children') $target = comment.$children!
else if (area === 'content') $target = comment.$content!
else if (area === 'replyTo') $target = comment.$replyTo!
if (!$target) return
// 阅读更多按钮
let $hideMoreOpenBtn = $target.querySelector<HTMLElement>('.atk-more-hide-open-btn')
const removeHideMore = () => {
$target.classList.remove('atk-comment-more-hide')
if ($hideMoreOpenBtn) $hideMoreOpenBtn.remove()
$target.style.height = ''
$target.style.overflow = ''
}
if (Utils.getHeight($target) > allowHeight) {
//console.log('内容过多,需要折叠', $target)
$target.classList.add('atk-comment-more-hide')
$target.style.height = `${allowHeight}px`
$target.style.overflow = 'hidden'
if (!$hideMoreOpenBtn) {
$hideMoreOpenBtn = Utils.createElement(`<div class="atk-more-hide-open-btn">阅读更多</span>`)
$hideMoreOpenBtn.onclick = (e) => {
e.stopPropagation()
removeHideMore()
// 阅读更多过后,再检查隐藏
if (comment.getIsRoot()) {
const children = comment.getChildren()
if (children.length > 1) {
this.eachComment(children, (c) => {
this.checkMoreHideEl(c, 'content', contentH || 200)
})
}
}
}
$target.append($hideMoreOpenBtn)
}
}
}
public removeHideMore($target: HTMLElement) {
const $hideMoreOpenBtn = $target.querySelector<HTMLElement>('.atk-more-hide-open-btn')
$target.classList.remove('atk-comment-more-hide')
if ($hideMoreOpenBtn) $hideMoreOpenBtn.remove()
$target.style.height = ''
$target.style.overflow = ''
}
/** 获取评论总数 (包括子评论) */
get commentsCount(): number {
return Number(this.data?.total) || 0
}
get parentCommentsCount() {
return Number(this.data?.total_parents) || 0
}
/** 是否还有更多的评论 */
get hasMoreComments(): boolean {
if (!this.data) return false
return (!this.flatMode ? this.data.total_parents : this.data.total) > (this.offset + this.pageSize)
}
/** 遍历操作 Comment (包括子评论) */
public eachComment(commentList: Comment[], action: (comment: Comment, levelList: Comment[]) => boolean|void) {
if (commentList.length === 0) return
commentList.every((item) => {
if (action(item, commentList) === false) return false
this.eachComment(item.getChildren(), action)
return true
})
}
/** 查找评论项 */
public findComment(id: number, src?: Comment[]): Comment|null {
if (!src) src = this.comments
let comment: Comment|null = null
this.eachComment(src, (item) => {
if (item.data.id === id) {
comment = item
return false
}
return true
})
return comment
}
/** 获取评论总数 */
public getCommentCount(): number {
let count = 0
this.eachComment(this.comments, () => { count++ })
return count
}
/** 删除评论 */
public deleteComment(comment: number|Comment) {
let findComment: Comment|null
if (typeof comment === 'number') {
findComment = this.findComment(comment)
if (!findComment) throw Error(`未找到评论 ${comment}`)
} else findComment = comment
findComment.getEl().remove()
this.eachComment(this.comments, (item, levelList) => {
if (item === findComment) {
levelList.splice(levelList.indexOf(item), 1)
return false
}
return true
})
if (this.data) this.data.total -= 1 // 评论数减 1
this.refreshUI()
}
/** 清空所有评论 */
public clearAllComments() {
this.$commentsWrap.innerHTML = ''
this.data = undefined
this.comments = []
}
public updateUnread(notifies: NotifyData[]) {
this.unread = notifies
// 高亮评论
if (this.unreadHighlight) {
this.eachComment(this.comments, (comment) => {
const notify = this.unread.find(o => o.comment_id === comment.data.id)
if (notify) {
comment.setUnread(true)
comment.setOpenURL(notify.read_link)
comment.openEvt = () => {
this.unread = this.unread.filter(o => o.comment_id !== comment.data.id) // remove
this.ctx.trigger('unread-update', {
notifies: this.unread
})
}
} else {
comment.setUnread(false)
}
})
}
}
public apiVersionCheck(versionData: ApiVersionData): boolean {
const needVersion = versionData?.fe_min_version || '0.0.0'
const needUpdate = Utils.versionCompare(needVersion, ARTALK_VERSION) === 1
if (needUpdate) {
// 需要更新
const errEl = Utils.createElement(`<div>前端 Artalk 版本已过时,请更新以获得完整体验<br/>`
+ `若您是站点管理员,请前往 “<a href="https://artalk.js.org/" target="_blank">官方文档</a>” 获取帮助`
+ `<br/><br/>`
+ `<span style="color: var(--at-color-meta);">前端版本 ${ARTALK_VERSION},需求版本 >= ${needVersion}</span><br/><br/>`
+ `</div>`)
const ignoreBtn = Utils.createElement('<span style="cursor:pointer;">忽略</span>')
ignoreBtn.onclick = () => {
Ui.setError(this.ctx, null)
this.ctx.conf.versionCheck = false
this.reqComments(0)
}
errEl.append(ignoreBtn)
Ui.setError(this.ctx, errEl, '<span class="atk-warn-title">Artalk Warn</span>')
}
return needUpdate
}
}
-45
View File
@@ -1,45 +0,0 @@
.artalk, .atk-layer-wrap {
* {
--at-color-font: #2a2e2e;
--at-color-deep: #2a2e2e;
--at-color-sub: #757575;
--at-color-grey: #999;
--at-color-meta: #757e91;
--at-color-border: #eceff2;
--at-color-light: #4285f4;
--at-color-bg: #FFF;
--at-color-bg-transl: rgba(255, 255, 255, 0.94);
--at-color-bg-grey: #F4F4F4;
--at-color-bg-grey-transl: rgba(244, 244, 244, 0.75);
--at-color-bg-light: rgba(29, 161, 242, 0.1);
--at-color-main: #0083ff;
--at-color-red: #ff5652;
--at-color-pink: #fa5a57;
--at-color-yellow: #ff7c37;
--at-color-green: #4caf50;
--at-color-quote: #687a86;
--at-color-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0), rgba(255, 255, 255));
}
&.atk-dark-mode * {
--at-color-font: #fff;
--at-color-deep: #e7e7e7;
--at-color-sub: #e7e7e7;
--at-color-grey: #fff;
--at-color-meta: #fff;
--at-color-border: #2d3235;
--at-color-light: #687a86;
--at-color-bg: #1e2224;
--at-color-bg-transl: rgba(30, 34, 36, 0.95);
--at-color-bg-grey: #46494e;
--at-color-bg-grey-transl: rgba(8, 8, 8, 0.95);
--at-color-bg-light: rgba(29, 161, 242, 0.1);
--at-color-main: #0083ff;
--at-color-red: #ff5652;
--at-color-pink: #fa5a57;
--at-color-yellow: #ff7c37;
--at-color-green: #4caf50;
--at-color-quote: #687a86;
--at-color-gradient: linear-gradient(180deg, rgba(30, 34, 36, 0), rgba(30, 34, 36, 1));
}
}
-28
View File
@@ -1,28 +0,0 @@
@color-font: var(--at-color-font);
@color-deep: var(--at-color-deep);
@color-sub: var(--at-color-sub);
@color-grey: var(--at-color-grey);
@color-meta: var(--at-color-meta);
@color-light: var(--at-color-light);
@color-border: var(--at-color-border);
@color-bg: var(--at-color-bg);
@color-bg-transl: var(--at-color-bg-transl);
@color-bg-grey: var(--at-color-bg-grey);
@color-bg-grey-transl: var(--at-color-bg-grey-transl);
@color-bg-light: var(--at-color-bg-light);
@color-main: var(--at-color-main);
@color-red: var(--at-color-red);
@color-pink: var(--at-color-pink);
@color-yellow: var(--at-color-yellow);
@color-green: var(--at-color-green);
@color-quote: var(--at-color-quote);
@color-gradient: var(--at-color-gradient);
@font-family: "Helvetica Neue", "PingFangSC-Light", "Hiragino Sans GB", "Microsoft YaHei", "WenQuanYi Micro Hei", sans-serif;
@font-size: 14px;
@line-height: 1.6em;
@block-margin: 15px;
@block-margin-mobile: 7px;
@block-padding: 10px;
@block-radius: 4px;
-301
View File
@@ -1,301 +0,0 @@
.atk-comment-wrap {
overflow: hidden;
position: relative;
border-bottom: 1px solid @color-border;
&.atk-flash-once {
animation: atkFlashOnce 1s ease-in-out 0s;
@keyframes atkFlashOnce {
from {
background: rgba(0, 131, 255, 0.2);
}
to {
background: transparent;
}
}
}
&.atk-unread:before {
content: ' ';
position: absolute;
left: 0;
top: 10%;
width: 3px;
height: 80%;
background: @color-main;
}
&.atk-openable {
cursor: pointer;
&:hover {
background: @color-bg-grey;
}
.atk-comment-more-hide:after {
background: transparent !important;
}
}
&:last-child {
border-bottom-color: transparent;
}
& > .atk-comment {
display: block;
padding: 12px 10px;
& > .atk-avatar {
display: block;
padding: 2px 0;
float: left;
img {
width: 48px;
height: 48px;
border-radius: 3px;
}
}
& > .atk-comment-main {
display: block;
margin-left: 62px;
& > .atk-header {
line-height: 1;
font-size: 13px;
margin-bottom: 3px;
overflow: hidden;
position: relative;
display: flex;
flex-wrap: wrap;
align-items: center;
.badge() {
color: @color-meta;
background: @color-bg-grey;
padding: 0 6px;
line-height: 18px;
border-radius: 3px;
&:not(:last-child) {
margin-right: 6px;
}
}
& > span {
display: flex;
margin-bottom: 2px;
color: @color-meta;
&:not(:last-child) {
margin-right: 6px;
}
&.atk-nick, &.atk-nick > a {
font-size: 14px;
color: @color-main;
text-decoration: none;
}
}
.atk-badge {
.badge();
background: #FF6C00;
color: #FFF;
}
.atk-ua-wrap {
@media only screen and (max-width: 768px) {
display: block;
}
.atk-ua {
.badge();
}
}
}
& > .atk-body {
display: block;
overflow: hidden;
position: relative;
img {
max-width: 100%;
}
& > .atk-content {
word-break: break-all;
&.atk-type-collapsed {
border: 3px solid @color-bg-grey;
border-bottom: 0;
padding: 5px 10px;
border-radius: @block-radius @block-radius 0 0;
margin-bottom: -5px;
}
& > *:first-child {
margin-top: 0;
}
& > *:last-child {
margin-bottom: 0;
}
.atk-more-hide-open-btn {
bottom: 5px;
}
}
& > .atk-pending {
color: var(--at-color-meta);
margin: 3px 0;
font-size: 13px;
padding: 10px 18px;
display: block;
background: @color-bg-grey;
border-left: 4px solid #f44336;
}
& > .atk-reply-to {
padding: 5px 15px;
border-left: 3px solid #eee;
margin-bottom: 10px;
position: relative;
& > .atk-meta {
font-size: 15px;
& > .atk-nick {
color: var(--at-color-main)
}
}
& > .atk-content {
margin-top: 5px;
}
}
& > .atk-collapsed {
margin: 3px 0;
font-size: 13px;
padding: 10px 18px;
display: block;
background: @color-bg-grey;
border-radius: @block-radius;
& > .atk-text {
color: @color-meta;
}
& > .atk-show-btn {
color: @color-main;
cursor: pointer;
user-select: none;
margin-left: 3px;
&:hover {
color: @color-main;
}
}
}
}
& > .atk-footer {
margin-top: 12px;
.atk-comment-actions {
display: flex;
flex-direction: row;
align-items: center;
& > span {
&, * {
color: @color-meta;
font-size: 13px;
line-height: 15px;
}
display: inline-flex;
cursor: pointer;
user-select: none;
&.atk-error, &.atk-error:hover {
color: @color-red;
}
&:not(:first-child):not(.atk-hide) {
margin-left: 8px;
padding-left: 8px;
border-left: 1px solid @color-border;
}
&:hover {
color: @color-deep;
}
}
}
}
}
}
.comment-small-size() {
border-bottom-color: transparent;
&:not(:last-child) {
margin-bottom: 5px;
}
& > .atk-comment {
padding-top: 4px;
padding-bottom: 4px;
& > .atk-avatar img {
width: 36px;
height: 36px;
}
& > .atk-comment-main {
margin-left: 47px;
}
}
}
.atk-comment-children {
.atk-comment-wrap {
margin-top: 10px;
border-left: 1px dashed var(--at-color-border);
.comment-small-size()
}
}
.atk-comment-more-hide:after {
position: absolute;
z-index: 1;
display: block;
overflow: hidden;
width: 100%;
content: " ";
bottom: 0;
height: 80px;
background: @color-gradient;
}
.atk-more-hide-open-btn {
z-index: 10;
position: absolute;
left: 50%;
bottom: 10px;
transform: translateX(-50%);
cursor: pointer;
border: 1px solid @color-border;
border-radius: @block-radius;
background: @color-bg;
padding: 1px 20px;
font-size: 15px;
color: @color-meta;
user-select: none;
}
}
+262 -796
View File
File diff suppressed because it is too large. Load diff