feat(forum): add complete forum workflows
This commit is contained in:
1 parent
a254cbd3e9
commit
ee65fc7fd2
39 files changed
+3425
-4
No files matched your search
@@ -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
|
||||
}
|
||||
})
|
||||
})()
|
||||
Reference in new issue
Block a user