Compare commits

...
Author SHA1 Message Date
qwqcode 29dec34f42 feat(ui/page_vote): add page voting feature
- Introduce page voting functionality
- Update configuration to enable/disable page voting
- Retrieve html elements for voting buttons and counts
- Enhance customization options for voting elements
- Reference: #983, #998, #997
2024-10-06 22:41:14 +08:00
12 changed files with 485 additions and 0 deletions

No files matched your search

+1
View File
@@ -40,6 +40,7 @@ export const en = defineConfig({
{ text: 'LaTeX', link: '/en/guide/frontend/latex.md' },
{ text: 'Image Lightbox', link: '/en/guide/frontend/lightbox.md' },
{ text: 'Image Lazy Load', link: '/en/guide/frontend/img-lazy-load.md' },
{ text: 'Voting', link: '/zh/guide/frontend/voting.md' },
{ text: 'IP Region', link: '/en/guide/frontend/ip-region.md' },
{ text: 'Localization', link: '/en/guide/frontend/i18n.md' },
{ text: 'Development Documentation', link: '/en/develop/index.md' },
+1
View File
@@ -40,6 +40,7 @@ export const zh = defineConfig({
{ text: 'LaTeX', link: '/zh/guide/frontend/latex.md' },
{ text: '图片灯箱', link: '/zh/guide/frontend/lightbox.md' },
{ text: '图片懒加载', link: '/zh/guide/frontend/img-lazy-load.md' },
{ text: '投票功能', link: '/zh/guide/frontend/voting.md' },
{ text: 'IP 属地', link: '/zh/guide/frontend/ip-region.md' },
{ text: '多语言', link: '/zh/guide/frontend/i18n.md' },
{ text: '开发文档', link: '/zh/develop/index.md' },
+11
View File
@@ -280,6 +280,17 @@ When querying comment counts and page views, Artalk's statistical component uses
To facilitate theme adaptation, you can customize the attribute name as needed, for example, replacing it with `data-path`, the HTML tag would be `<span data-path="/t/1.html"></span>`.
### pageVote
**Page Voting Buttons**
- Type: `Boolean | { upBtnEl: string; downBtnEl: string; upCountEl: string; downCountEl: string; activeClass: string }`
- Default: `true`
Enable the page voting feature (upvote / downvote).
For details, see: [Page Voting](./voting.md#page-voting)
### vote
**Voting Buttons**
+123
View File
@@ -0,0 +1,123 @@
# Voting Feature
Artalk supports voting on comments and pages, allowing users to click the "Up" or "Down" buttons to cast their votes. The comment list can be sorted based on the number of votes, which helps users better assess the quality of the comments.
## Comment Voting
The comment voting feature is enabled by default, allowing users to vote on comments.
You can find the "UI Settings" in the Dashboard to modify the "Vote Button" option to enable or disable comment voting.
Environment variable for the vote button:
```
ATK_FRONTEND_VOTE=1
```
Configuration file for the vote button:
```yaml
frontend:
vote: true
```
### Down Button
By default, Artalk does not display the Down button. You can find the "UI Settings" in the Dashboard to modify the "Vote Down Button" option to enable it.
Environment variable for the Down button:
```
ATK_FRONTEND_VOTE_DOWN=1
```
Configuration file for the Down button:
```yaml
frontend:
voteDown: true
```
## Page Voting
Artalk supports voting on pages. To enable page voting, you need to add elements in the page to display the voting buttons, which Artalk will automatically initialize on load:
```html
<div>
<span class="artalk-page-vote-up"></span>
<span class="artalk-page-vote-down"></span>
</div>
```
### Voted Status Styling
When users click the page voting button, the element will be given an `active` class to indicate that the user has voted. For example:
```html
<span class="artalk-page-vote-up active"></span>
```
You can customize the voted button style using CSS:
```css
.artalk-page-vote-up.active {
color: #0083ff;
}
```
The default added class name is `active`, but it can be changed using `pageVote.activeClass` in the client configuration:
```js
Artalk.init({
pageVote: {
activeClass: 'active',
},
})
```
### Custom Element Selectors
By default, Artalk searches for `.artalk-page-vote-up` and `.artalk-page-vote-down` as the voting button elements.
You can customize the voting button selectors by modifying the `pageVote.upBtnEl` and `pageVote.downBtnEl` configuration in the client:
```js
Artalk.init({
pageVote: {
upBtnEl: '.artalk-page-vote-up',
downBtnEl: '.artalk-page-vote-down',
},
})
```
### Further Customizing Page Voting Buttons
If the voting buttons do not contain any child elements, Artalk will output the text "Up (n)" into the element.
If you want to output the vote count into a separate element, you can add a tag inside the button, for example:
```html
<div class="artalk-page-vote">
<span class="artalk-page-vote-up">
👍 (<span class="artalk-page-vote-up-count"></span>)
</span>
<span class="artalk-page-vote-down">
👎 (<span class="artalk-page-vote-down-count"></span>)
</span>
</div>
```
To further customize, you can replace the text with icons or add other styles.
The default selectors for vote counts are `.artalk-page-vote-up-count` and `.artalk-page-vote-down-count`.
You can modify `pageVote.upCountEl` and `pageVote.downCountEl` to customize the vote count output elements:
```js
Artalk.init({
pageVote: {
upCountEl: '.artalk-page-vote-up-count',
downCountEl: '.artalk-page-vote-down-count',
},
})
```
+11
View File
@@ -278,6 +278,17 @@ Artalk 统计组件查询评论数和浏览量时,会通过该属性名来查
为了便于主题适配,可根据需要自定义属性名,例如将其替换为 `data-path`,则 HTML 标签为 `<span data-path="/t/1.html"></span>`。
### pageVote
**页面投票**
- 类型:`Boolean | { upBtnEl: string; downBtnEl: string; upCountEl: string; downCountEl: string; activeClass: string }`
- 默认值: `true`
启用页面投票功能,用户可以为页面投票。
详情参考:[页面投票](./voting.md#页面投票)
### vote
**投票按钮**
+123
View File
@@ -0,0 +1,123 @@
# 投票功能
Artalk 支持对评论和页面进行投票,用户可以点击“赞同”或“反对”按钮进行投票。评论列表支持通过投票数进行排序。评论投票功能可以帮助用户更好地了解评论的质量。
## 评论投票
评论投票功能默认启用,用户可以对评论进行投票。
你可以在控制台设置界面找到「界面配置」,修改「投票按钮」选项来启用或禁用评论投票功能。
投票按钮的环境变量:
```
ATK_FRONTEND_VOTE=1
```
投票按钮的配置文件:
```yaml
frontend:
vote: true
```
### 反对按钮
默认情况下,Artalk 不会显示反对按钮。你可以在控制台设置界面找到「界面配置」,修改「反对按钮」选项来启用反对按钮。
反对按钮的环境变量:
```
ATK_FRONTEND_VOTE_DOWN=1
```
反对按钮的配置文件:
```yaml
frontend:
voteDown: true
```
## 页面投票
Artalk 支持对页面进行投票,你需要在页面中添加元素来显示页面投票按钮,Artalk 在加载时会自动初始化页面投票按钮:
```html
<div>
<span class="artalk-page-vote-up"></span>
<span class="artalk-page-vote-down"></span>
</div>
```
### 已投票状态样式
当用户点击了页面投票按钮后,元素会被添加 `active` 类名,表示用户已经投票,例如:
```html
<span class="artalk-page-vote-up active"></span>
```
你可以通过 CSS 样式来自定义按钮的已投票样式:
```css
.artalk-page-vote-up.active {
color: #0083ff;
}
```
默认添加的类名为 `active`,可以在客户端通过 `pageVote.activeClass` 来修改:
```js
Artalk.init({
pageVote: {
activeClass: 'active',
},
})
```
### 自定义元素选择器
Artalk 默认查找 `.artalk-page-vote-up` 和 `.artalk-page-vote-down` 作为投票按钮元素。
修改客户端的 `pageVote.upBtnEl` 和 `pageVote.downBtnEl` 配置可以自定义投票按钮选择器:
```js
Artalk.init({
pageVote: {
upBtnEl: '.artalk-page-vote-up',
downBtnEl: '.artalk-page-vote-down',
},
})
```
### 进一步自定义页面投票按钮
如果投票按钮内没有子元素,Artalk 会输出文字 "赞同 (n)" 到元素中。
如果你想输出投票数量到单独的元素,可以在按钮中添加一个标签,例如:
```html
<div class="artalk-page-vote">
<span class="artalk-page-vote-up">
👍 (<span class="artalk-page-vote-up-count"></span>)
</span>
<span class="artalk-page-vote-down">
👎 (<span class="artalk-page-vote-down-count"></span>)
</span>
</div>
```
更进一步,你可以将文字修改为图标,或者添加其他样式。
投票数选择器默认为 `.artalk-page-vote-up-count` 和 `.artalk-page-vote-down-count`,
可以修改 `pageVote.upCountEl` 和 `pageVote.downCountEl` 自定义投票数输出元素:
```js
Artalk.init({
pageVote: {
upCountEl: '.artalk-page-vote-up-count',
downCountEl: '.artalk-page-vote-down-count',
},
})
```
+8
View File
@@ -139,6 +139,14 @@
<span id="ArtalkPV"></span>
|
<span id="ArtalkCount"></span>
<div class="artalk-page-vote">
<span class="artalk-page-vote-up"
>赞同 (<span class="artalk-page-vote-up-count"></span>)</span
>
<span class="artalk-page-vote-down"
>反对 (<span class="artalk-page-vote-down-count"></span>)</span
>
</div>
</div>
<div class="social">
<a
+1
View File
@@ -66,6 +66,7 @@ export function handleConfFormServer(conf: ArtalkConfigPartial): ArtalkConfigPar
'pvEl',
'countEl',
'statPageKeyAttr',
'pageVote',
]
Object.keys(conf).forEach((k) => {
if (ExcludedKeys.includes(k as any)) delete conf[k]
+2
View File
@@ -26,6 +26,8 @@ const defaults: RequiredExcept<ArtalkConfig, ExcludedKeys> = {
emoticons: 'https://cdn.jsdelivr.net/gh/ArtalkJS/Emoticons/grps/default.json',
pageVote: true,
vote: true,
voteDown: false,
uaBadge: true,
+2
View File
@@ -6,6 +6,7 @@ import { PvCountWidget } from './stat'
import { VersionCheck } from './version-check'
import { AdminOnlyElem } from './admin-only-elem'
import { DarkMode } from './dark-mode'
import { PageVoteWidget } from './page-vote'
import type { ArtalkPlugin } from '@/types'
export const DefaultPlugins: ArtalkPlugin[] = [
@@ -17,4 +18,5 @@ export const DefaultPlugins: ArtalkPlugin[] = [
PvCountWidget,
VersionCheck,
DarkMode,
PageVoteWidget,
]
+182
View File
@@ -0,0 +1,182 @@
import type { ArtalkPlugin, ListFetchedArgs } from '@/types'
import type { Api } from '@/api'
import $t from '@/i18n'
interface PageVoteOptions {
/** Up Vote Button Selector */
upBtnEl: string
/** Down Vote Button Selector */
downBtnEl: string
/** Up Vote Count Selector */
upCountEl: string
/** Down Vote Count Selector */
downCountEl: string
/** Active class name if the vote is already cast */
activeClass?: string
}
const defaults: PageVoteOptions = {
upBtnEl: '.artalk-page-vote-up',
downBtnEl: '.artalk-page-vote-down',
upCountEl: '.artalk-page-vote-up-count',
downCountEl: '.artalk-page-vote-down-count',
activeClass: 'active',
}
type VoteBtnHandler = (evt: MouseEvent) => void
interface PageVoteState {
upBtnEl?: HTMLElement | null
downBtnEl?: HTMLElement | null
upCountEl?: HTMLElement | null
downCountEl?: HTMLElement | null
voteUpHandler?: VoteBtnHandler
voteDownHandler?: VoteBtnHandler
activeClass?: string
}
export const PageVoteWidget: ArtalkPlugin = (ctx) => {
let state: PageVoteState = initState()
let cleanup: (() => void) | undefined
ctx.watchConf(['pageVote'], (conf) => {
const options = { ...defaults, ...(typeof conf.pageVote === 'object' ? conf.pageVote : {}) }
cleanup?.()
if (conf.pageVote) {
state = loadOptions(state, options, voteUpHandler, voteDownHandler)
cleanup = () => (state = resetState(state))
}
})
ctx.on('unmounted', () => {
cleanup?.()
ctx.off('list-fetched', listFetchedHandler)
})
ctx.on('list-fetched', listFetchedHandler)
// List fetched handler
let currPageId = 0
function listFetchedHandler({ data }: ListFetchedArgs) {
if (!ctx.getConf().pageVote || !checkEls(state) || !data) return
if (currPageId !== data.page.id) {
// Initialize vote status in a new page
currPageId = data.page.id
updateVoteStatus(state, data.page.vote_up, data.page.vote_down, false, false) // reset initial status
fetchVoteStatus(state, currPageId, ctx.getApi())
} else {
// Update vote status in the same page
updateVoteStatus(state, data.page.vote_up, data.page.vote_down)
}
}
// Vote button click handlers
const handlerOptions = () => ({
state,
pageId: ctx.getData().getPage()?.id || 0,
httpApi: ctx.getApi(),
})
const voteUpHandler = voteBtnHandler('up', handlerOptions)
const voteDownHandler = voteBtnHandler('down', handlerOptions)
}
function loadOptions(
_state: PageVoteState,
opts: PageVoteOptions,
voteUpHandler: VoteBtnHandler,
voteDownHandler: VoteBtnHandler,
): PageVoteState {
const state: PageVoteState = { ..._state } // clone to keep immutable
state.upBtnEl = document.querySelector<HTMLElement>(opts.upBtnEl)
state.downBtnEl = document.querySelector<HTMLElement>(opts.downBtnEl)
state.upCountEl = document.querySelector<HTMLElement>(opts.upCountEl)
state.downCountEl = document.querySelector<HTMLElement>(opts.downCountEl)
state.activeClass = opts.activeClass
state.voteUpHandler = voteUpHandler
state.voteDownHandler = voteDownHandler
state.upBtnEl?.addEventListener('click', voteUpHandler)
state.downBtnEl?.addEventListener('click', voteDownHandler)
return state
}
function checkEls(state: PageVoteState) {
return state.upBtnEl || state.downBtnEl || state.upCountEl || state.downCountEl
}
function initState(): PageVoteState {
return {}
}
function resetState(state: PageVoteState): PageVoteState {
state.voteUpHandler && state.upBtnEl?.removeEventListener('click', state.voteUpHandler)
state.voteDownHandler && state.downBtnEl?.removeEventListener('click', state.voteDownHandler)
return initState()
}
function updateVoteStatus(
state: PageVoteState,
up: number | string,
down: number | string,
isUp?: boolean,
isDown?: boolean,
) {
// Up vote count
if (state.upCountEl) {
state.upCountEl.innerText = String(up)
} else if (state.upBtnEl) {
state.upBtnEl.innerText = `${$t('voteUp')} (${up})`
}
// Down vote count
if (state.downCountEl) {
state.downCountEl.innerText = String(down)
} else if (state.downBtnEl) {
state.downBtnEl.innerText = `${$t('voteDown')} (${down})`
}
if (typeof isUp === 'boolean')
state.upBtnEl?.classList.toggle(state.activeClass || 'active', isUp)
if (typeof isDown === 'boolean')
state.downBtnEl?.classList.toggle(state.activeClass || 'active', isDown)
}
interface VoteBtnHandlerOptions {
state: PageVoteState
pageId: number
httpApi: Api
}
const voteBtnHandler =
(choice: 'up' | 'down', options: () => VoteBtnHandlerOptions) => (evt: MouseEvent) => {
evt.preventDefault()
const { state, pageId, httpApi } = options()
if (!pageId) return
httpApi.votes
.createVote('page', pageId, choice, {
...httpApi.getUserFields(),
})
.then(({ data }) => {
updateVoteStatus(state, data.up, data.down, data.is_up, data.is_down)
})
.catch((err) => {
window.alert($t('voteFail'))
console.error('[ArtalkPageVote]', err)
})
}
function fetchVoteStatus(state: PageVoteState, pageId: number, httpApi: Api) {
httpApi.votes
.getVote('page', pageId)
.then(({ data }) => {
updateVoteStatus(state, data.up, data.down, data.is_up, data.is_down)
})
.catch((err) => {
console.error('[ArtalkPageVote]', err)
})
}
+20
View File
@@ -73,6 +73,26 @@ export interface ArtalkConfig {
/** Downvote button for comments */
voteDown: boolean
/** Page Vote Widget */
pageVote:
| {
/** Up Vote Button Selector */
upBtnEl: string
/** Down Vote Button Selector */
downBtnEl: string
/** Up Vote Count Selector */
upCountEl: string
/** Down Vote Count Selector */
downCountEl: string
/** Active class name if the vote is already cast */
activeClass: string
}
| boolean
/** Preview feature for comments */
preview: boolean