feat(forum): rebuild plate, post and compose pages

Match the reference layouts for the remaining forum routes: plate pages
get the blurred header card, moderator row and the affix tab bar with
per-tab filtering, topping posts and moderator-only pending/trash tabs;
post pages get the article shell with breadcrumbs, cover, meta row,
vote box, tag row and footer actions; compose pages get the reference
editor plus settings sidebar (submit/draft, status, access, plate,
topic, tag, type, vote) with a cover picker. Saving now honours draft
state and the featured image through the REST contract.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
kate522andClaude Fable 5 committed 2026-11-11 16:17:55 +08:00
1 parent eceddb5f7d
commit 5d3358d844
13 files changed
+891 -199

No files matched your search

+87
View File
@@ -95,6 +95,93 @@
typeSelect?.addEventListener('change', syncType)
syncType()
// 发帖页:下拉式设置项(阅读权限 / 版块 / 话题 / 标签 / 类型)统一走隐藏输入。
const syncHidden = (name, value) => {
root.querySelectorAll(`input[type="hidden"][name="${name}"]`).forEach((input) => { input.value = value })
}
const syncTypeBox = () => {
const box = root.querySelector('.forum-type-vote')
const current = root.querySelector('input[type="hidden"][name="type"]')?.value || 'normal'
if (box) box.hidden = current !== 'vote'
}
root.addEventListener('click', (event) => {
const type = event.target.closest('[data-forum-type-set]')
if (type) {
root.querySelectorAll('[data-forum-type-set]').forEach((item) => item.classList.toggle('active', item === type))
syncHidden('type', type.dataset.forumTypeSet)
syncTypeBox()
return
}
const view = event.target.closest('[data-forum-view]')
if (view) {
root.querySelectorAll('[data-forum-view]').forEach((item) => item.classList.toggle('active', item === view))
syncHidden('view_limit', view.dataset.forumView)
const label = root.querySelector('span[name="view_limit"]')
if (label) label.textContent = view.textContent.trim()
return
}
const plate = event.target.closest('[data-forum-plate]')
if (plate) {
root.querySelectorAll('[data-forum-plate]').forEach((item) => item.classList.toggle('active', item === plate))
syncHidden('plate_id', plate.dataset.forumPlate)
const label = root.querySelector('span[name="plate_id"]')
if (label) label.textContent = plate.textContent.trim()
return
}
const status = event.target.closest('[data-forum-status]')
if (status) {
root.querySelectorAll('[data-forum-status]').forEach((item) => item.classList.toggle('active', item === status))
syncHidden('post_status', status.dataset.forumStatus)
return
}
const pick = event.target.closest('[data-forum-cover-pick]')
if (pick) {
root.querySelector('[data-forum-cover-file]')?.click()
return
}
const draft = event.target.closest('[data-forum-draft]')
if (draft) {
const form = draft.closest('[data-forum-form]')
if (form) {
syncHidden('post_status', 'draft')
form.requestSubmit()
}
}
})
root.addEventListener('change', (event) => {
const term = event.target.closest('[data-forum-topics],[data-forum-tags]')
if (term) {
const field = term.matches('[data-forum-topics]') ? 'topics' : 'tags'
const values = [...root.querySelectorAll(`[data-forum-${field}]:checked`)].map((input) => input.value)
syncHidden(field, values.join(','))
term.closest('label')?.classList.toggle('active', term.checked)
const label = root.querySelector(`[data-forum-${field}-label]`)
if (label) label.textContent = values.length ? values.slice(0, 2).join('、') : '请选择'
}
})
root.addEventListener('change', async (event) => {
const cover = event.target.closest('[data-forum-cover-file]')
if (!cover?.files?.[0]) return
const data = new FormData()
data.append('file', cover.files[0])
try {
const response = await fetch(config.api + 'upload/image', { 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 || '上传失败')
const preview = root.querySelector('.forum-cover-preview')
if (preview) {
preview.classList.remove('is-empty')
preview.innerHTML = '<img src="' + payload.data.url + '" alt="">'
}
const hidden = root.querySelector('input[name="featured_image_id"]')
if (hidden) hidden.value = payload.data.id || ''
} catch (error) {
message(cover.closest('.edit-posts-featured') || root, error.message)
} finally {
cover.value = ''
}
})
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 || '下载附件' }