(() => { 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 = '

' 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 = '

扫码支付

支付完成后刷新页面

' 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 } }) })()