feat(forum): add complete forum workflows

This commit is contained in:
kate522 committed 2026-11-11 00:09:43 +08:00
1 parent a254cbd3e9
commit ee65fc7fd2
39 files changed
+3425 -4

No files matched your search

@@ -0,0 +1,27 @@
import { renderFaIcon, schemaPage } from './helpers'
const pages = [
['general', '全局设置'],
['home', '首页与列表'],
['publishing', '发布与审核'],
['access', '权限与版主'],
['replies', '回复与互动'],
['payment', '付费与积分'],
['urls', '链接设置'],
]
export const forumSection = {
key: 'forum',
label: '社区论坛',
icon: renderFaIcon('fa fas fa-comments'),
route: '/forum',
children: pages.map(([key, label]) => schemaPage({
key: `forum-${key}`,
name: `Forum_${key}`,
label,
title: label,
route: `/forum/${key}`,
canonicalDomain: 'forum',
section: key,
})),
}
@@ -8,6 +8,7 @@ import { extensionsSection } from './extensions'
import { themeInfoSection } from './theme-info'
import { docsSection } from './docs'
import { toolsSection } from './tools'
import { forumSection } from './forum'
const SECTION_DEFAULT_CHILDREN = {
global: 'global-common',
@@ -18,6 +19,7 @@ const SECTION_DEFAULT_CHILDREN = {
payment: 'payment-knowledge',
extensions: 'extensions-system',
tools: 'tools-import-export',
forum: 'forum-general',
'theme-info': 'theme-info-dashboard',
docs: 'docs-dashboard',
}
@@ -43,6 +45,7 @@ export const ADMIN_SECTIONS = [
attachSectionDefaultRoute(permissionsSection),
attachSectionDefaultRoute(usersSection),
attachSectionDefaultRoute(paymentSection),
attachSectionDefaultRoute(forumSection),
attachSectionDefaultRoute(extensionsSection),
attachSectionDefaultRoute(toolsSection),
attachSectionDefaultRoute(themeInfoSection),
+2 -2
View File
@@ -170,7 +170,7 @@ function resolveSectionFromPath(path) {
/** Load schema from the Vue-side registry */
const pageMeta = computed(() => {
const schema = (isEmailPage.value || resolvedDomain.value === 'page_display')
const schema = (isEmailPage.value || ['page_display', 'forum'].includes(resolvedDomain.value))
? serverSchema.value
: getSchema(resolvedDomain.value, resolvedSection.value)
if (schema && schema.groups && schema.groups.length > 0) {
@@ -192,7 +192,7 @@ const pageMeta = computed(() => {
/** Default values from schema */
const defaults = computed(() => {
const schema = (isEmailPage.value || resolvedDomain.value === 'page_display')
const schema = (isEmailPage.value || ['page_display', 'forum'].includes(resolvedDomain.value))
? serverSchema.value
: getSchema(resolvedDomain.value, resolvedSection.value)
if (!schema) return {}
+100
View File
@@ -0,0 +1,100 @@
#forum{--forum-ink:var(--key-color,#24282c);--forum-muted:var(--muted-color,#737b82);--forum-line:var(--muted-4-color,#e6e9eb);--forum-accent:var(--focus-color,#2869d1);color:var(--forum-ink)}
#forum .jio-forum__wrap{width:min(1200px,calc(100% - 32px));margin:22px auto 70px}
#forum .jio-forum__breadcrumbs{display:flex;gap:9px;align-items:center;color:var(--forum-muted);font-size:13px;margin-bottom:18px}
#forum .jio-forum__breadcrumbs a{color:inherit}
#forum .jio-forum__layout{display:grid;grid-template-columns:minmax(0,1fr) 286px;gap:24px;align-items:start}
#forum .jio-forum__main{min-width:0}
#forum .jio-forum__hero,#forum .jio-forum__page-head{padding:20px 0;border-bottom:1px solid var(--forum-line)}
#forum h1{font-size:27px;line-height:1.25;margin:0 0 10px}
#forum .jio-forum__hero p,#forum .jio-forum__page-head p{margin:0;color:var(--forum-muted)}
#forum .jio-forum__tabs{display:flex;gap:26px;overflow:auto;white-space:nowrap;border-bottom:1px solid var(--forum-line);margin-bottom:16px}
#forum .jio-forum__tabs a{padding:14px 0;color:var(--forum-muted);border-bottom:2px solid transparent}
#forum .jio-forum__tabs .is-active{color:var(--forum-accent);border-color:var(--forum-accent);font-weight:600}
#forum .jio-forum__chips{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}
#forum .jio-forum__chips a,#forum .jio-forum__badge,#forum .jio-forum__badges a,#forum .jio-forum__badges span{padding:4px 8px;background:#f1f5f7;color:#48717c;border-radius:4px;font-size:12px}
#forum .jio-forum__post{display:flex;gap:13px;padding:20px 2px;border-bottom:1px solid var(--forum-line)}
#forum .jio-forum__avatar{flex:0 0 44px}#forum .jio-forum__avatar img{width:44px;height:44px;border-radius:50%}
#forum .jio-forum__post-body{min-width:0;flex:1}#forum .jio-forum__post-top{display:flex;gap:12px;color:var(--forum-muted);font-size:12px}
#forum .jio-forum__post h2{font-size:17px;line-height:1.45;margin:7px 0}#forum .jio-forum__post h2 a{color:var(--forum-ink)}
#forum .jio-forum__post p{color:var(--forum-muted);font-size:14px;line-height:1.6;margin:0 0 10px}
#forum .jio-forum__post-meta{display:flex;align-items:center;gap:10px;color:var(--forum-muted);font-size:12px;flex-wrap:wrap}
#forum .jio-forum__post-meta a{color:var(--forum-accent)}
#forum .jio-forum__aside{border-left:1px solid var(--forum-line);padding-left:20px}
#forum .jio-forum__new,#forum .jio-forum__primary{display:inline-flex;align-items:center;justify-content:center;background:var(--forum-accent);color:#fff;border:0;border-radius:4px;min-height:40px;padding:8px 16px;cursor:pointer}
#forum .jio-forum__new{width:100%}#forum .jio-forum__aside-section{padding:22px 0;border-bottom:1px solid var(--forum-line)}
#forum .jio-forum__aside-section h2{font-size:16px;margin:0 0 12px}#forum .jio-forum__aside-section a{display:block;padding:6px 0;color:var(--forum-ink)}
#forum .jio-forum__plates{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:16px}
#forum .jio-forum__plate{display:flex;gap:13px;padding:14px;border:1px solid var(--forum-line);border-radius:6px}
#forum .jio-forum__plate-cover{width:80px;height:80px;flex:none;background:#edf2f5;border-radius:5px;overflow:hidden}
#forum .jio-forum__plate-cover img{width:100%;height:100%;object-fit:cover}#forum .jio-forum__plate h2{font-size:16px;margin:3px 0 8px}
#forum .jio-forum__plate p{font-size:13px;color:var(--forum-muted);margin:0}
#forum .jio-forum__single{padding:20px 0}#forum .jio-forum__single-meta{display:flex;align-items:center;gap:10px;color:var(--forum-muted);font-size:13px}
#forum .jio-forum__single-meta img{border-radius:50%}#forum .jio-forum__single-head h1{margin:18px 0}
#forum .jio-forum__badges,#forum .jio-forum__actions{display:flex;gap:8px;flex-wrap:wrap}
#forum .jio-forum__single-content{font-size:15px;line-height:1.8;padding:22px 0;overflow-wrap:anywhere}
#forum .jio-forum__actions{border-top:1px solid var(--forum-line);padding:16px 0}
#forum .jio-forum__action{border:1px solid var(--forum-line);border-radius:4px;background:transparent;color:var(--forum-muted);padding:7px 12px;cursor:pointer}
#forum .jio-forum__action:hover{color:var(--forum-accent);border-color:var(--forum-accent)}
#forum .jio-forum__form{display:grid;gap:15px;margin:20px 0}#forum .jio-forum__form label{display:grid;gap:6px;font-size:14px;font-weight:600}
#forum .jio-forum__form input,#forum .jio-forum__form select,#forum .jio-forum__form textarea{width:100%;border:1px solid #d6dce1;border-radius:4px;padding:10px 12px;background:var(--body-bg,#fff);color:inherit;font:inherit}
#forum .jio-forum__form-extra[hidden]{display:none}#forum .jio-forum__message{color:#b64343;min-height:20px}
#forum .jio-forum__vote{border-top:1px solid var(--forum-line);padding:20px 0}#forum .jio-forum__vote h2{font-size:17px}
#forum .jio-forum__vote label{display:flex;gap:12px;padding:12px;border:1px solid var(--forum-line);margin-bottom:8px;border-radius:4px}
#forum .jio-forum__vote-count{margin-left:auto}#forum .jio-forum__locked{padding:30px;text-align:center;background:#f7f9fa}
#forum .jio-forum__pagination ul{list-style:none;display:flex;gap:7px;padding:0;margin:24px 0}
#forum .jio-forum__pagination a,#forum .jio-forum__pagination span{display:block;padding:6px 10px;border:1px solid var(--forum-line);border-radius:4px}
#forum .jio-forum__empty{padding:36px;text-align:center;color:var(--forum-muted)}
@media(max-width:800px){#forum .jio-forum__layout{grid-template-columns:1fr}#forum .jio-forum__aside{border-left:0;padding-left:0}#forum .jio-forum__plates{grid-template-columns:1fr}}
@media(max-width:480px){#forum .jio-forum__wrap{width:calc(100% - 24px)}#forum h1{font-size:23px}#forum .jio-forum__post{padding:16px 0}#forum .jio-forum__tabs{gap:18px}}
#forum .jio-forum__create-plate{width:100%;margin-top:10px}
#forum .jio-forum__section-title{font-size:18px;margin:28px 0 0}
#forum .jio-forum__reply{display:flex;gap:12px;padding:17px 0;border-bottom:1px solid var(--forum-line)}
#forum .jio-forum__reply-avatar img{border-radius:50%}
#forum .jio-forum__reply-main{flex:1;min-width:0}
#forum .jio-forum__reply-head{display:flex;gap:12px;align-items:center;font-size:13px}
#forum .jio-forum__reply-head time{color:var(--forum-muted)}#forum .jio-forum__reply-head strong{color:#147e70}
#forum .jio-forum__reply-text{padding:10px 0;line-height:1.7;overflow-wrap:anywhere}
#forum .jio-forum__reply-actions button{border:0;background:none;color:var(--forum-accent);padding:4px 10px 4px 0;cursor:pointer}
#forum .jio-forum__reply-form{display:grid;gap:12px;margin:24px 0}
#forum .jio-forum__reply-form h3{font-size:17px;margin:0}
#forum .jio-forum__reply-form textarea{width:100%;border:1px solid var(--forum-line);padding:12px;border-radius:4px;background:var(--body-bg,#fff);color:inherit}
#forum .jio-forum__reply-form button{justify-self:start}
#forum .jio-forum__widget-link{display:flex;justify-content:space-between;gap:10px;padding:7px 0;color:var(--forum-ink)}
#forum .jio-forum__widget-link span{color:var(--forum-muted)}
.jio-forum__dialog{width:min(440px,calc(100% - 28px));max-height:90vh;border:1px solid #dce2e5;border-radius:6px;padding:22px;background:var(--body-bg,#fff);color:inherit;box-shadow:0 20px 60px #0003}
.jio-forum__dialog::backdrop{background:#10182088}
.jio-forum__dialog h2{font-size:20px;margin:0 0 18px;overflow-wrap:anywhere}
.jio-forum__dialog form,.jio-forum__dialog-fields{display:grid;gap:14px}
.jio-forum__dialog label{display:grid;gap:6px;font-size:14px}
.jio-forum__dialog input,.jio-forum__dialog select,.jio-forum__dialog textarea{width:100%;min-height:39px;border:1px solid #cfd7dc;padding:9px;border-radius:4px;background:var(--body-bg,#fff);color:inherit}
.jio-forum__dialog-actions{display:flex;justify-content:flex-end;gap:10px;margin-top:10px}
.jio-forum__dialog-actions button{border:1px solid #d6dce0;border-radius:4px;padding:8px 14px;cursor:pointer}
.jio-forum__qr{width:180px;height:180px;margin:auto}
.jio-forum__user-panel h2{font-size:18px;margin:0 0 14px}
.jio-forum__user-row{display:flex;justify-content:space-between;gap:12px;padding:12px 0;border-bottom:1px solid #e6e9eb}
#forum .jio-forum__applications{margin:18px 0;padding:14px 0;border-top:1px solid var(--forum-line)}
#forum .jio-forum__applications h2{font-size:16px;margin:0 0 8px}
#forum .jio-forum__application{display:flex;align-items:center;gap:10px;padding:6px 0}
#forum .jio-forum__application button{border:1px solid var(--forum-line);border-radius:4px;background:transparent;padding:4px 9px;cursor:pointer}
#forum .jio-forum__reply-parent{color:var(--forum-muted);font-size:12px;margin:8px 0 0}
#forum .jio-forum__hot-reply{margin:0 0 10px 58px;padding:9px 12px;background:#f5f8f9;color:var(--forum-muted);font-size:13px;border-radius:4px}
#forum .jio-forum__pending{padding:16px 0;border-bottom:1px solid var(--forum-line)}
#forum .jio-forum__pending h2{font-size:16px;margin:0 0 10px}
#forum .jio-forum__pending>div{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:6px 0}
#forum .jio-forum__pending button{border:1px solid var(--forum-line);background:transparent;border-radius:4px;padding:5px 9px;cursor:pointer}
#forum .jio-forum__editor-tools{display:flex;gap:8px;flex-wrap:wrap}
#forum .wp-editor-wrap{border:1px solid var(--forum-line)}
body.dark-theme #forum .jio-forum__chips a,body.dark-theme #forum .jio-forum__badge,body.dark-theme #forum .jio-forum__badges a,body.dark-theme #forum .jio-forum__badges span{background:var(--muted-bg-2-color);color:var(--forum-accent)}
body.dark-theme #forum .jio-forum__plate-cover{background:var(--muted-bg-2-color)}
body.dark-theme #forum .jio-forum__locked,body.dark-theme #forum .jio-forum__hot-reply{background:var(--muted-bg-2-color)}
body.dark-theme #forum .jio-forum__form input,body.dark-theme #forum .jio-forum__form select,body.dark-theme #forum .jio-forum__form textarea,body.dark-theme #forum .jio-forum__reply-form textarea{background:var(--main-bg-color);border-color:var(--forum-line);color:var(--forum-ink)}
body.dark-theme .jio-forum__dialog{background:var(--main-bg-color);color:var(--key-color);border-color:var(--muted-4-color)}
body.dark-theme .jio-forum__dialog input,body.dark-theme .jio-forum__dialog select,body.dark-theme .jio-forum__dialog textarea{background:var(--main-bg-color);color:var(--key-color);border-color:var(--muted-4-color)}
body.dark-theme #forum .jio-forum__action{color:var(--forum-muted)}
#forum .jio-forum__term-management{margin:16px 0;padding:16px 0;border-bottom:1px solid var(--forum-line)}
#forum .jio-forum__term-management h2{font-size:16px}
#forum .jio-forum__term-management form{display:flex;gap:8px;align-items:end;flex-wrap:wrap}
#forum .jio-forum__term-management label{display:grid;gap:5px;font-size:13px}
#forum .jio-forum__term-management input,#forum .jio-forum__term-management select{min-height:34px;padding:5px 8px;border:1px solid var(--forum-line);border-radius:4px;background:var(--main-bg-color);color:var(--forum-ink)}
#forum .jio-forum__term-row{display:flex;gap:8px;align-items:center;padding:5px 0}
#forum .jio-forum__term-row button{border:1px solid var(--forum-line);border-radius:4px;background:transparent;padding:4px 7px;cursor:pointer}
+266
View File
@@ -0,0 +1,266 @@
(() => {
const config = window.jioForum || {}
const root = document.getElementById('forum')
if (!root) return
const send = async (path, method = 'GET', data = {}, idempotencyKey = '') => {
const headers = { 'Content-Type': 'application/json', 'X-WP-Nonce': config.nonce }
if (idempotencyKey) headers['Idempotency-Key'] = idempotencyKey
const response = await fetch(config.api + path, {
method,
credentials: 'same-origin',
headers,
body: method === 'GET' ? undefined : JSON.stringify(data),
})
const payload = await response.json()
if (!response.ok || payload.success === false) throw new Error(payload.message || '操作失败')
return payload
}
const message = (element, value) => {
const target = element.querySelector('.jio-forum__message') || element.closest('.jio-forum__single, .jio-forum__replies, .jio-forum__page-head')?.querySelector('.jio-forum__message')
if (target) target.textContent = value
else if (value) window.alert(value)
}
const modal = (title, fields, submitText = '确认') => new Promise((resolve) => {
const dialog = document.createElement('dialog')
dialog.className = 'jio-forum__dialog'
const form = document.createElement('form')
form.innerHTML = '<h2></h2><div class="jio-forum__dialog-fields"></div><div class="jio-forum__dialog-actions"><button type="button" data-cancel>取消</button><button type="submit" class="jio-forum__primary"></button></div>'
form.querySelector('h2').textContent = title
form.querySelector('[type="submit"]').textContent = submitText
const body = form.querySelector('.jio-forum__dialog-fields')
fields.forEach(({ name, label, type = 'text', options = [], value = '' }) => {
const wrapper = document.createElement('label')
wrapper.textContent = label
const control = type === 'select' ? document.createElement('select') : document.createElement(type === 'textarea' ? 'textarea' : 'input')
control.name = name
control.required = true
if (type === 'select') options.forEach(({ value: optionValue, label: optionLabel }) => {
const option = document.createElement('option')
option.value = optionValue
option.textContent = optionLabel
control.append(option)
})
else if (type === 'text') control.type = 'text'
control.value = value
wrapper.append(control)
body.append(wrapper)
})
dialog.append(form)
root.append(dialog)
const close = (value) => { dialog.close(); dialog.remove(); resolve(value) }
form.querySelector('[data-cancel]').addEventListener('click', () => close(null))
dialog.addEventListener('cancel', (event) => { event.preventDefault(); close(null) })
form.addEventListener('submit', (event) => { event.preventDefault(); close(Object.fromEntries(new FormData(form))) })
dialog.showModal()
})
const checkout = async (kind, id) => {
const preview = (await send(`${kind}s/${id}/checkout`)).data
const methods = (preview.methods || []).map((value) => ({ value, label: {
points: '积分', balance: '站内余额', alipay: '支付宝', wechatpay: '微信支付', paypal: 'PayPal', stripe: '银行卡',
}[value] || value }))
const data = await modal(`解锁 ${preview.title} · ${preview.price}${preview.mode === 'points' ? '积分' : '元'}`, [
{ name: 'payment_method', label: '支付方式', type: 'select', options: methods },
], '确认支付')
if (!data) return
const key = crypto.randomUUID()
const result = (await send(`${kind}s/${id}/checkout`, 'POST', data, key)).data
if (result.mode === 'redirect' && result.payment_url) window.location.href = result.payment_url
else if (result.mode === 'form' && result.form_html) {
const wrapper = document.createElement('div')
wrapper.innerHTML = result.form_html
document.body.append(wrapper)
wrapper.querySelector('form')?.submit()
} else if (result.mode === 'qrcode' && result.code_url && window.QRCode) {
const dialog = document.createElement('dialog')
dialog.className = 'jio-forum__dialog'
dialog.innerHTML = '<h2>扫码支付</h2><div class="jio-forum__qr"></div><p>支付完成后刷新页面</p><button type="button">完成</button>'
root.append(dialog)
new window.QRCode(dialog.querySelector('.jio-forum__qr'), { text: result.code_url, width: 180, height: 180 })
dialog.querySelector('button').addEventListener('click', () => window.location.reload())
dialog.showModal()
} else if (result.mode) window.location.href = config.orders
else window.location.reload()
}
const typeSelect = root.querySelector('select[name="type"]')
const syncType = () => {
const vote = root.querySelector('[data-forum-type="vote"]')
if (vote) vote.hidden = typeSelect?.value !== 'vote'
}
typeSelect?.addEventListener('change', syncType)
syncType()
const insertMedia = (kind, url, name) => {
const element = document.createElement(kind === 'image' ? 'img' : kind === 'video' ? 'video' : 'a')
if (kind === 'resource') { element.href = url; element.textContent = name || '下载附件' }
else element.src = url
if (kind === 'video') element.controls = true
if (kind === 'image') element.alt = name || ''
const html = element.outerHTML
const editor = window.tinymce?.get('jio_forum_content')
if (editor && !editor.isHidden()) editor.insertContent(html)
else {
const textarea = root.querySelector('textarea[name="content"]')
if (textarea) textarea.setRangeText(html, textarea.selectionStart, textarea.selectionEnd, 'end')
}
}
root.addEventListener('change', async (event) => {
const input = event.target.closest('[data-forum-file]')
if (!input?.files?.[0]) return
const kind = input.dataset.kind
const data = new FormData()
data.append('file', input.files[0])
try {
const response = await fetch(config.api + `upload/${kind}`, { method: 'POST', credentials: 'same-origin', headers: { 'X-WP-Nonce': config.nonce }, body: data })
const payload = await response.json()
if (!response.ok || !payload.success) throw new Error(payload.message || '上传失败')
insertMedia(kind, payload.data.url, payload.data.name)
} catch (error) {
message(input.closest('.jio-forum__form') || root, error.message)
} finally {
input.value = ''
}
})
root.addEventListener('submit', async (event) => {
const form = event.target.closest('[data-forum-form]')
if (!form) return
event.preventDefault()
const button = form.querySelector('[type="submit"]')
const kind = form.dataset.forumForm
const id = Number(form.dataset.id || 0)
if (kind === 'post') window.tinymce?.get('jio_forum_content')?.save()
const body = Object.fromEntries(new FormData(form).entries())
if (kind === 'vote' && form.querySelector('input[name="option[]"]')) {
body.options = new FormData(form).getAll('option[]')
}
button.disabled = true
try {
const result = kind === 'vote' ? await send(`posts/${id}/vote`, 'POST', body)
: kind === 'plate' ? await send(id ? `plates/${id}` : 'plates', id ? 'PATCH' : 'POST', body)
: kind === 'term' ? await send('terms', 'POST', body)
: kind === 'moderator' ? await send(`plates/${id}/moderators`, 'POST', { ...body, active: true })
: kind === 'reply' ? await send(`posts/${id}/replies`, 'POST', body)
: await send(id ? `posts/${id}` : 'posts', id ? 'PATCH' : 'POST', body)
if (result.data?.url) window.location.href = result.data.url
else if (kind === 'moderator') window.location.reload()
else if (kind === 'term') window.location.reload()
else if (kind === 'vote') window.location.reload()
else if (kind === 'reply' && result.data?.status === 'publish') window.location.reload()
else message(form, result.message || '已保存')
} catch (error) {
message(form, error.message)
} finally {
button.disabled = false
}
})
root.addEventListener('click', async (event) => {
const upload = event.target.closest('[data-forum-upload]')
if (upload) {
const input = root.querySelector('[data-forum-file]')
if (input) {
input.dataset.kind = upload.dataset.forumUpload
input.accept = { image: 'image/*', video: 'video/*', resource: '' }[input.dataset.kind] || ''
input.click()
}
return
}
const open = event.target.closest('[data-forum-open]')
if (open) { root.querySelector(`[data-forum-dialog="${open.dataset.forumOpen}"]`)?.showModal(); return }
const close = event.target.closest('[data-forum-close]')
if (close) { close.closest('dialog')?.close(); return }
const reply = event.target.closest('[data-forum-reply-to]')
if (reply) {
const form = root.querySelector('[data-forum-form="reply"]')
if (form) { form.querySelector('[name="parent_id"]').value = reply.dataset.forumReplyTo; form.scrollIntoView({ behavior: 'smooth' }); form.querySelector('textarea')?.focus() }
return
}
const button = event.target.closest('[data-forum-action]')
if (!button) return
const action = button.dataset.forumAction
const id = Number(button.dataset.id || 0)
if (action === 'delete' && !window.confirm('确定删除此内容?')) return
button.disabled = true
try {
let result
if (action === 'unlock') { await checkout('post', id); return }
if (action === 'plate-unlock') { await checkout('plate', id); return }
if (action === 'plate-audit') {
result = await send(`plates/${id}/audit`, 'POST', { status: 'publish' })
button.closest('.jio-forum__pending > div')?.remove()
} else if (action === 'plate-delete') {
const data = await modal('删除版块', [
{ name: 'posts_action', label: '关联帖子', type: 'select', options: [{ value: 'trash', label: '一并移入回收站' }, { value: 'move', label: '移动到其他版块' }] },
{ name: 'target_plate_id', label: '目标版块', type: 'select', options: [{ value: '0', label: '无需选择' }, ...(config.plates || []).filter((plate) => plate.id !== id).map((plate) => ({ value: String(plate.id), label: plate.title }))] },
], '确认删除')
if (!data) return
result = await send(`plates/${id}/delete`, 'POST', data)
button.dataset.forumAction = 'plate-restore'
button.textContent = '撤销删除'
} else if (action === 'plate-restore') {
result = await send(`plates/${id}/restore`, 'POST')
button.dataset.forumAction = 'plate-delete'
button.textContent = '删除版块'
} else if (action === 'reply-like' || action === 'reply-hot') {
result = await send(`replies/${id}/${action === 'reply-like' ? 'like' : 'hot'}`, 'POST')
} else if (action === 'reply-delete') {
if (!window.confirm('确定删除此回复?')) return
result = await send(`replies/${id}/delete`, 'POST')
button.closest('.jio-forum__reply')?.remove()
} else if (action === 'term-edit') {
const name = button.closest('.jio-forum__term-row')?.querySelector('span')?.textContent || ''
const data = await modal('编辑分类', [{ name: 'name', label: '名称', value: name }], '保存')
if (!data) return
result = await send(`terms/${id}`, 'PATCH', data)
button.closest('.jio-forum__term-row').querySelector('span').textContent = data.name
} else if (action === 'term-delete') {
if (!window.confirm('确定删除这个分类?')) return
result = await send(`terms/${id}`, 'DELETE')
button.closest('.jio-forum__term-row')?.remove()
} else if (action === 'follow') {
try { result = await send(`plates/${id}/follow`, 'POST') }
catch (error) { if (error.message.includes('付费')) { await checkout('plate', id); return } throw error }
} else if (action === 'adopt') result = await send(`posts/${id}/adopt`, 'POST', { comment_id: Number(button.dataset.commentId) })
else if (action === 'report') {
const data = await modal('举报帖子', [{ name: 'category', label: '类别', type: 'select', options: [{ value: '违规内容', label: '违规内容' }, { value: '侵权', label: '侵权' }, { value: '其他', label: '其他' }] }, { name: 'reason', label: '原因', type: 'textarea' }], '提交举报')
if (!data) return
result = await send(`posts/${id}/report`, 'POST', data)
} else if (action === 'apply') {
const data = await modal('申请版主', [{ name: 'reason', label: '申请理由', type: 'textarea' }], '提交申请')
if (!data) return
result = await send(`plates/${id}/apply`, 'POST', data)
} else if (action === 'move') {
const data = await modal('移动帖子', [{ name: 'plate_id', label: '目标版块', type: 'select', options: (config.plates || []).map((plate) => ({ value: String(plate.id), label: plate.title })) }], '确认移动')
if (!data) return
result = await send(`posts/${id}/move`, 'POST', data)
} else if (action === 'application') {
result = await send(`plates/${id}/applications/${Number(button.dataset.userId)}`, 'POST', { status: button.dataset.status })
button.closest('.jio-forum__application')?.remove()
} else if (action === 'moderator-remove') {
result = await send(`plates/${id}/moderators`, 'POST', { user_id: Number(button.dataset.userId), active: false })
button.closest('.jio-forum__application')?.remove()
} else if (action === 'comments') {
const status = button.textContent.includes('关闭') ? 'closed' : 'open'
result = await send(`posts/${id}/comments`, 'POST', { status })
button.textContent = status === 'closed' ? '开放回复' : '关闭回复'
} else result = await send(`posts/${id}/${action}`, 'POST', action === 'score' ? { direction: button.dataset.direction || 'up' } : {})
if (action === 'delete') {
button.dataset.forumAction = 'restore'
button.textContent = '撤销删除'
} else if (action === 'adopt' || action === 'audit') window.location.reload()
else button.textContent = result.data?.label || result.message || button.textContent
message(button, result.message || '')
} catch (error) {
message(button, error.message)
if (error.message.includes('登录') && config.login) window.location.href = config.login
} finally {
button.disabled = false
}
})
})()