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:
1 parent
eceddb5f7d
commit
5d3358d844
13 files changed
+891
-199
No files matched your search
@@ -181,6 +181,7 @@
|
||||
.forum-posts.detail .avatar-img{--this-size:38px}
|
||||
.lists-imgs.count-3{--img-count:3}
|
||||
.lists-imgs.count-2,.lists-imgs.count-4{--img-count:2}
|
||||
}
|
||||
@media(max-width:480px){
|
||||
#forum .jio-forum__wrap{width:calc(100% - 24px)}
|
||||
#forum h1{font-size:23px}
|
||||
@@ -282,3 +283,88 @@ body.dark-theme .panel.panel-plate,body.dark-theme .plate-lists .plate-item,body
|
||||
.forum-header .count-mates > item + item{padding-left:15px;margin-left:15px}
|
||||
}
|
||||
body.dark-theme .forum-header,body.dark-theme .forum-header .header-content{background:var(--main-bg-color)}
|
||||
|
||||
/* ── 版块 / 分类页面 ──────────────────────────────────────────── */
|
||||
.forum-listing .jio-forum__breadcrumbs{margin:0 0 12px}
|
||||
.forum-listing .jio-forum__list-main{min-width:0}
|
||||
.plate-tab{padding:0}
|
||||
.plate-tab .affix-header-sm{padding:0 15px;background:var(--main-bg-color);border-radius:var(--main-radius) var(--main-radius) 0 0}
|
||||
.plate-tab .affix-header-sm.affix,.plate-tab .affix-header-sm.affix-bottom{padding:8px 25px}
|
||||
.plate-tab .tab-nav-theme{margin:0;border-bottom:1px solid var(--main-border-color)}
|
||||
.plate-tab .tab-nav-theme li a{font-size:16px;color:var(--muted-2-color);display:block;padding:12px 14px}
|
||||
.plate-tab .tab-nav-theme li.active a{font-weight:bold;color:var(--focus-color)}
|
||||
.plate-tab .tab-content{padding:10px 15px}
|
||||
.plate-tab .tab-content > .tab-pane{display:none}
|
||||
.plate-tab .tab-content > .tab-pane.active{display:block}
|
||||
.plate-tab .forum-tab-pane{min-height:120px}
|
||||
.forum-header.blur-header{overflow:hidden;color:#fff}
|
||||
.forum-header.blur-header .header-content{background:transparent;color:#fff;padding:28px 30px 22px}
|
||||
.forum-header.blur-header .forum-title,.forum-header.blur-header .forum-title a{color:#fff}
|
||||
.forum-header.blur-header .desc,.forum-header.blur-header .px12-sm{color:rgba(255,255,255,.86)}
|
||||
.forum-header .abs-blur-bg img{width:100%;height:100%;object-fit:cover}
|
||||
.forum-header .forum-mask{position:absolute;inset:0;background:rgba(76,80,87,.6)}
|
||||
.forum-header.blur-header .plate-thumb{width:90px;height:90px;flex-shrink:0}
|
||||
.forum-header.blur-header .plate-thumb > img{border-radius:var(--main-radius)}
|
||||
.forum-header .moderator-btns{display:flex;flex-wrap:wrap;align-items:center;gap:6px}
|
||||
.forum-header .moderator-btns .avatar-mini{--this-size:22px;margin-right:4px}
|
||||
.forum-header .moderator-btns .avatar-mini img{border-radius:100px}
|
||||
.forum-header .moderator-btns .but{font-size:12px;padding:3px 10px}
|
||||
|
||||
/* ── 帖子详情页 ───────────────────────────────────────────────── */
|
||||
.forum-post .single-wrap{width:100%}
|
||||
.forum-single-cover{border-radius:var(--main-radius);overflow:hidden}
|
||||
.forum-article{padding:20px 26px}
|
||||
.forum-article .article-header{margin-bottom:15px;padding-bottom:12px;margin-top:5px;border-bottom:1px solid var(--main-border-color)}
|
||||
.forum-article .article-title{font-size:24px;line-height:1.4;margin:0 0 10px;font-weight:600}
|
||||
.forum-article .article-title a{color:var(--key-color)}
|
||||
.forum-article-meta,.forum-article-meta a{color:var(--muted-2-color);font-size:13px}
|
||||
.forum-article-meta .meta-left{display:flex;align-items:center;gap:8px}
|
||||
.forum-article-meta .meta-right{display:flex;align-items:center}
|
||||
.forum-article-meta .item{margin-left:18px;display:inline-flex;align-items:center;color:var(--muted-2-color)}
|
||||
.forum-article .wp-posts-content{font-size:15px;line-height:1.9;overflow-wrap:anywhere}
|
||||
.forum-article .article-tags{display:flex;flex-wrap:wrap;gap:6px}
|
||||
.forum-article .article-tags .but{font-size:12px;padding:4px 10px}
|
||||
.footer-actions{margin:18px -10px 10px;padding-top:14px;border-top:1px solid var(--main-border-color)}
|
||||
.footer-actions .item{display:flex;align-items:center;padding:4px 8px;margin:0 4px;color:var(--muted-color);border:0;background:none;cursor:pointer;border-radius:4px}
|
||||
.footer-actions .item:hover{background:var(--muted-border-color)}
|
||||
.footer-actions .item > i{font-size:18px;margin-right:5px}
|
||||
.footer-actions .item text{font-size:12px}
|
||||
.single-footer .but{font-size:12px;padding:4px 12px}
|
||||
.vote-box.forum-vote{padding:20px 0}
|
||||
.vote-box.forum-vote h2{font-size:17px}
|
||||
.vote-item{position:relative;overflow:hidden;padding:8px 3.5em 8px 15px;margin-bottom:6px;border-radius:50px;background:var(--muted-border-color);color:var(--muted-color);display:block;cursor:pointer;text-overflow:ellipsis;white-space:nowrap}
|
||||
.vote-item input{margin-right:8px}
|
||||
.vote-progress{position:absolute;left:0;top:0;bottom:0;background:rgba(130,130,130,.15);transition:width .6s,background .2s;border-radius:50px;width:0;z-index:-1}
|
||||
.vote-item .vote-count{position:absolute;right:14px;font-size:.9em;opacity:.8}
|
||||
.vote-box.forum-vote button{display:block;margin:16px auto 0}
|
||||
|
||||
/* ── 发帖 / 编辑页 ────────────────────────────────────────────── */
|
||||
.forum-edit .content-layout{margin-right:calc(var(--sidebar-width,311px) + 15px)}
|
||||
.forum-edit .sidebar{float:left;width:var(--sidebar-width,311px);margin-left:calc(0px - var(--sidebar-width,311px))}
|
||||
.forum-edit .editor-main-box{padding:20px 26px}
|
||||
.forum-edit .edit-posts-featured{display:flex;align-items:center;gap:12px}
|
||||
.forum-edit .forum-cover-preview{width:180px;height:101px;border-radius:var(--main-radius);overflow:hidden;background:var(--muted-border-color);display:flex;align-items:center;justify-content:center;color:var(--muted-3-color)}
|
||||
.forum-edit .forum-cover-preview img{width:100%;height:100%;object-fit:cover}
|
||||
.forum-edit .edit-posts-title{position:relative;margin:10px 0 14px}
|
||||
.forum-edit .line-form-input{width:100%;border:0;border-bottom:1px solid var(--main-border-color);background:transparent;font-size:22px;font-weight:600;padding:8px 0;resize:none;overflow:hidden;outline:none;color:var(--key-color)}
|
||||
.forum-edit .line-form-input:focus{border-bottom-color:var(--focus-color)}
|
||||
.forum-edit .line-form-line{display:none}
|
||||
.forum-edit .sidebar .zib-widget{padding:0}
|
||||
.forum-edit .drop-btn{padding:12px 15px;cursor:pointer}
|
||||
.forum-edit .drop-select .dropdown-menu{width:100%;left:0;right:0}
|
||||
.forum-edit .select-drop-box .but{display:block;width:100%;text-align:left;margin-bottom:4px;border-radius:4px}
|
||||
.forum-edit .select-drop-box .but.active{background:var(--focus-color);color:#fff}
|
||||
.forum-edit .type-drop{padding:12px 15px}
|
||||
.forum-edit .type-drop .but.active{--this-bg:var(--focus-color);--this-color:#fff}
|
||||
.forum-edit .forum-type-vote .form-control{width:100%;margin-bottom:6px}
|
||||
.forum-edit .jio-forum__editor-tools .but{font-size:13px}
|
||||
@media (max-width:800px){
|
||||
.forum-edit .content-layout{margin-right:0}
|
||||
.forum-edit .sidebar{width:100%;margin-left:0;margin-top:16px}
|
||||
.forum-edit .editor-main-box{padding:15px}
|
||||
.forum-article{padding:15px}
|
||||
.forum-article .article-title{font-size:20px}
|
||||
.forum-header.blur-header .header-content{padding:20px 15px}
|
||||
.forum-header.blur-header .plate-thumb{width:64px;height:64px}
|
||||
}
|
||||
body.dark-theme .forum-header.blur-header .forum-mask{background:rgba(20,22,26,.72)}
|
||||
@@ -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 || '下载附件' }
|
||||
|
||||
@@ -53,6 +53,24 @@ final class SchemaForum {
|
||||
self::field('jio_forum_home_follow_plate', '版块栏目显示已关注版块', 'switcher', true),
|
||||
self::field('jio_forum_home_recommend_plate', '关注栏目显示版块推荐', 'switcher', true),
|
||||
self::field('jio_forum_show_plate_cover', '显示版块封面', 'switcher', true),
|
||||
self::field('jio_forum_plate_tab_active', '版块页默认栏目', 'number', 1, ['min' => 1, 'max' => 20, 'description' => '打开版块页面时默认显示第几个栏目']),
|
||||
self::field('jio_forum_plate_tab_items', '版块页栏目', 'repeater', self::default_plate_tabs(), [
|
||||
'addText' => '添加栏目',
|
||||
'description' => '版块页顶部的栏目条;留空则使用「最新发布 / 最近回复 / 热门 / 精华」默认栏目',
|
||||
'fields' => [
|
||||
self::field('title', '栏目标题', 'input', ''),
|
||||
self::field('orderby', '排序方式', 'select', 'date', ['options' => self::options([
|
||||
'date' => '发布时间',
|
||||
'modified' => '修改时间',
|
||||
'last_reply' => '最新回复',
|
||||
'views' => '阅读量',
|
||||
'score' => '评分',
|
||||
'comment_count' => '回复数量',
|
||||
])]),
|
||||
self::field('filter', '内容筛选', 'select', '', ['options' => self::options(['' => '全部', 'essence' => '精华'])]),
|
||||
self::field('style', '列表样式', 'radio', 'detail', ['options' => self::options(['detail' => '详细内容', 'mini' => '简约风格'])]),
|
||||
],
|
||||
]),
|
||||
self::field('jio_forum_search_enabled', '搜索包含帖子', 'switcher', true),
|
||||
]),
|
||||
'publishing' => self::section('publishing', '发布与审核', [
|
||||
@@ -95,6 +113,16 @@ final class SchemaForum {
|
||||
]];
|
||||
}
|
||||
|
||||
/** Initial plate-page tab rows; the listing falls back to these when unset. */
|
||||
private static function default_plate_tabs(): array {
|
||||
return [
|
||||
['title' => '最新发布', 'orderby' => 'date', 'filter' => '', 'style' => 'detail'],
|
||||
['title' => '最近回复', 'orderby' => 'last_reply', 'filter' => '', 'style' => 'detail'],
|
||||
['title' => '热门', 'orderby' => 'views', 'filter' => '', 'style' => 'detail'],
|
||||
['title' => '精华', 'orderby' => 'modified', 'filter' => 'essence', 'style' => 'detail'],
|
||||
];
|
||||
}
|
||||
|
||||
private static function section(string $key, string $title, array $fields): array {
|
||||
return ['key' => $key, 'title' => $title, 'version' => '1.0.0', 'option_key' => 'jio_settings', 'groups' => [['key' => $key, 'fields' => $fields]]];
|
||||
}
|
||||
|
||||
+55
-2
@@ -73,8 +73,61 @@ final class Cards {
|
||||
return '<h2 class="' . esc_attr($class) . '"><a' . $link_class . ' href="' . esc_url($url) . '" title="' . esc_attr($post->post_title) . '">' . $badges . esc_html($post->post_title) . '</a></h2>';
|
||||
}
|
||||
|
||||
public static function plate_item(\WP_Post $plate): string {
|
||||
$url = get_permalink($plate);
|
||||
/** Plate page header: blurred cover, title, stats and moderators (reference layout). */
|
||||
public static function plate_header(\WP_Post $plate): string {
|
||||
$id = $plate->ID;
|
||||
PlateStats::prime([$id]);
|
||||
$stats = PlateStats::get($id);
|
||||
$followers = (int) get_post_meta($id, '_jio_forum_follow_count', true);
|
||||
$plate_name = (string) jio_get_setting('jio_forum_plate_name', '版块') ?: '版块';
|
||||
$thumb = self::thumbnail($plate);
|
||||
$cats = '';
|
||||
$terms = wp_get_post_terms($id, Core::CATEGORY);
|
||||
foreach (is_array($terms) ? $terms : [] as $term) {
|
||||
$cats .= '<a class="badg badg-sm b-theme" href="' . esc_url(get_term_link($term)) . '">' . esc_html($term->name) . '</a>';
|
||||
}
|
||||
$meta = '<div class="px12-sm mt10"><item class="mate-posts mr10">' . esc_html(sprintf('%s %s', $plate_name, jio_cut_count($stats['posts']))) . '</item>'
|
||||
. '<item class="mate-reply mr10">' . esc_html(sprintf('今日 %s', jio_cut_count($stats['today']))) . '</item>'
|
||||
. ($followers > 0
|
||||
? '<item class="mate-follow mr10">' . esc_html(sprintf('关注 %s', jio_cut_count($followers))) . '</item>'
|
||||
: '') . '</div>';
|
||||
$excerpt = wp_trim_words(wp_strip_all_tags($plate->post_excerpt ?: $plate->post_content), 40);
|
||||
$html = '<div class="plate-header-wrap"><div class="forum-header blur-header relative-h mb20">';
|
||||
$html .= '<div class="abs-blur-bg">' . $thumb . '</div><div class="absolute forum-mask"></div>';
|
||||
if (get_post_meta($id, '_jio_forum_hot', true)) {
|
||||
$html .= '<span class="img-badge top jb-red px12-sm">热门</span>';
|
||||
}
|
||||
$html .= '<div class="relative header-content"><div class="flex ac"><div class="item-info flex1">';
|
||||
if ($cats !== '') $html .= '<div class="mb6">' . $cats . '</div>';
|
||||
$html .= '<h1 class="forum-title em14"><a href="' . esc_url(get_permalink($plate)) . '">' . esc_html($plate->post_title) . '</a></h1>';
|
||||
$html .= $meta;
|
||||
if ($excerpt !== '') $html .= '<div class="desc px12-sm mt6">' . esc_html($excerpt) . '</div>';
|
||||
$html .= '</div><div class="plate-thumb flex0 ml10">' . $thumb . '</div></div>';
|
||||
$html .= self::plate_moderator_btns($plate);
|
||||
return $html . '</div></div></div>';
|
||||
}
|
||||
|
||||
/** Plate moderator avatars row under the header. */
|
||||
private static function plate_moderator_btns(\WP_Post $plate): string {
|
||||
$ids = array_values(array_unique(array_filter(array_merge(
|
||||
[(int) $plate->post_author],
|
||||
array_map('absint', (array) get_post_meta($plate->ID, '_jio_forum_moderators', true))
|
||||
))));
|
||||
if (!$ids) return '';
|
||||
$btns = '';
|
||||
foreach ($ids as $user_id) {
|
||||
$user = get_userdata($user_id);
|
||||
if (!$user) continue;
|
||||
$btns .= '<a class="but c-white mr6 mt6" href="' . esc_url(get_author_posts_url($user_id)) . '" title="' . esc_attr($user->display_name) . '"><span class="avatar-mini avatar-img">' . get_avatar($user_id, 40) . '</span><span class="ml6">' . esc_html($user->display_name) . '</span></a>';
|
||||
}
|
||||
$html = '<div class="mt10 moderator-btns em09">' . $btns;
|
||||
if (is_user_logged_in() && !Access::is_moderator(get_current_user_id(), $plate->ID) && jio_get_setting('jio_forum_moderator_apply', true)) {
|
||||
$html .= '<button type="button" class="but hollow c-white mt6" data-forum-action="apply" data-id="' . (int) $plate->ID . '">申请版主</button>';
|
||||
}
|
||||
return $html . '</div>';
|
||||
}
|
||||
|
||||
public static function plate_item(\WP_Post $plate): string { $url = get_permalink($plate);
|
||||
$counts = PlateStats::get($plate->ID);
|
||||
$today = $counts['today'] > 0 ? '<span class="badg but-plate hide-sm flex0" title="' . esc_attr(sprintf('今日新帖 %d', $counts['today'])) . '"><i class="fa fas fa-comment-dots c-red mr3"></i>' . esc_html(sprintf('今日:%s', jio_cut_count($counts['today']))) . '</span>' : '';
|
||||
$excerpt = wp_trim_words(wp_strip_all_tags($plate->post_excerpt ?: $plate->post_content), 30);
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
<?php
|
||||
|
||||
namespace Jio\Forum;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/** Forum compose/edit page (reference two-column editor + settings sidebar). */
|
||||
final class EditView {
|
||||
public static function render(): void {
|
||||
$edit_id = absint(get_query_var('jio_forum_edit'));
|
||||
$post = $edit_id > 0 ? get_post($edit_id) : null;
|
||||
if ($edit_id > 0 && (!$post instanceof \WP_Post || !Access::can_manage_post(get_current_user_id(), $edit_id))) {
|
||||
status_header(403);
|
||||
echo '<main id="forum" class="jio-forum forum-edit"><div class="container"><div class="zib-widget full-widget-sm text-center" style="min-height:60vh;">' . jio_get_null('抱歉!您暂无编辑此内容的权限', 60, 'null-post.svg') . '</div></div></main>';
|
||||
get_footer();
|
||||
return;
|
||||
}
|
||||
$user_id = get_current_user_id();
|
||||
$type = $post ? (string) get_post_meta($post->ID, '_jio_forum_type', true) : 'normal';
|
||||
$plate_id = $post ? (int) get_post_meta($post->ID, '_jio_forum_plate_id', true) : absint(is_scalar($_GET['plate'] ?? 0) ? $_GET['plate'] : 0);
|
||||
$view_limit = $post ? (string) get_post_meta($post->ID, '_jio_forum_view_limit', true) : 'all';
|
||||
$vote_options = $post ? (array) get_post_meta($post->ID, '_jio_forum_vote_options', true) : [];
|
||||
$topics = $post ? wp_get_post_terms($post->ID, Core::TOPIC, ['fields' => 'names']) : [];
|
||||
$tags = $post ? wp_get_post_terms($post->ID, Core::TAG, ['fields' => 'names']) : [];
|
||||
|
||||
wp_enqueue_editor();
|
||||
echo '<main id="forum" class="jio-forum forum-edit"><form id="bbs-posts-edit" data-forum-form="post" data-id="' . (int) $edit_id . '"><div class="container"><div class="content-wrap"><div class="content-layout">';
|
||||
echo '<div class="zib-widget full-widget-sm editor-main-box" style="min-height:60vh">';
|
||||
if ($user_id <= 0) {
|
||||
echo jio_get_null('请先登录后再发布', 40, 'null-user.svg') . '<p class="text-center"><a class="but jb-blue padding-lg" href="' . esc_url(jio_get_login_url(Core::edit_url($edit_id))) . '"><i class="fa fas fa-sign-in-alt mr6"></i>登录</a></p>';
|
||||
echo '</div>';
|
||||
self::close();
|
||||
return;
|
||||
}
|
||||
// Cover
|
||||
$cover = $post ? get_the_post_thumbnail_url($post, 'large') : '';
|
||||
echo '<div class="relative edit-posts-featured mb20" data-forum-cover>';
|
||||
echo '<div class="forum-cover-preview' . ($cover ? '' : ' is-empty') . '">' . ($cover ? '<img src="' . esc_url($cover) . '" alt="">' : '<i class="fa far fa-image em3x"></i>') . '</div>';
|
||||
echo '<button type="button" class="but hollow c-blue padding-lg" data-forum-cover-pick><i class="fa fas fa-image mr6"></i>' . ($cover ? '更换封面' : '添加封面') . '</button>';
|
||||
echo '<input type="file" accept="image/*" data-forum-cover-file hidden><input type="hidden" name="featured_image_id" value="' . (int) get_post_thumbnail_id($edit_id) . '"></div>';
|
||||
// Title
|
||||
echo '<div class="relative edit-posts-title"><textarea class="line-form-input input-lg new-title" name="title" rows="1" maxlength="120" tabindex="1" placeholder="' . esc_attr((string) jio_get_setting('jio_forum_title_placeholder', '请输入标题')) . '">' . esc_textarea($post ? $post->post_title : '') . '</textarea><i class="line-form-line"></i></div>';
|
||||
// Editor
|
||||
wp_editor($post ? $post->post_content : '', 'jio_forum_content', [
|
||||
'textarea_name' => 'content',
|
||||
'textarea_rows' => 20,
|
||||
'editor_height' => wp_is_mobile() ? 460 : 486,
|
||||
'media_buttons' => false,
|
||||
'quicktags' => false,
|
||||
'default_editor' => 'tinymce',
|
||||
'tinymce' => ['placeholder' => (string) jio_get_setting('jio_forum_content_placeholder', '请输入内容')],
|
||||
]);
|
||||
echo '<div class="jio-forum__editor-tools mt10">';
|
||||
foreach (['image' => ['插入图片', 'fa-image'], 'video' => ['插入视频', 'fa-video'], 'resource' => ['插入附件', 'fa-paperclip']] as $kind => [$label, $icon]) {
|
||||
$setting = 'jio_forum_upload_' . ($kind === 'resource' ? 'file' : $kind);
|
||||
if (jio_get_setting($setting, $kind === 'image')) {
|
||||
echo '<button type="button" class="but hollow px12" data-forum-upload="' . esc_attr($kind) . '"><i class="fa fas ' . esc_attr($icon) . ' mr6"></i>' . esc_html($label) . '</button>';
|
||||
}
|
||||
}
|
||||
echo '<input type="file" data-forum-file hidden></div>';
|
||||
self::hidden_fields($post, $type, $view_limit);
|
||||
echo '</div>'; // editor-main-box
|
||||
|
||||
// Sidebar: submit, status, access, plate, topic, tag, type, vote
|
||||
echo '<div class="sidebar show-sidebar">';
|
||||
echo self::submit_box($post);
|
||||
echo self::status_box($post);
|
||||
echo self::allow_view_box($view_limit);
|
||||
echo self::plate_box($plate_id);
|
||||
echo self::term_box('topics', '话题', Core::TOPIC, $topics, 'fa-hashtag');
|
||||
echo self::term_box('tags', '标签', Core::TAG, $tags, 'fa-tag');
|
||||
echo self::type_box($type);
|
||||
echo self::vote_box($vote_options, $post);
|
||||
\Jio\WidgetSystem\WidgetAreas::render('forum_edit_sidebar');
|
||||
echo '</div>';
|
||||
self::close();
|
||||
}
|
||||
|
||||
private static function hidden_fields(?\WP_Post $post, string $type, string $view_limit): void {
|
||||
echo '<input type="hidden" name="type" value="' . esc_attr($type) . '">';
|
||||
echo '<input type="hidden" name="view_limit" value="' . esc_attr($view_limit) . '">';
|
||||
echo '<p class="jio-forum__message" role="status" aria-live="polite"></p>';
|
||||
}
|
||||
|
||||
private static function submit_box(?\WP_Post $post): string {
|
||||
$last = $post ? get_the_modified_time('Y-m-d H:i:s', $post->ID) : '';
|
||||
$html = '<div class="zib-widget mb10-sm mb20"><div class="but-average mb20 main-radius">';
|
||||
if (!$post || !in_array($post->post_status, ['publish', 'pending'], true)) {
|
||||
$html .= '<button type="button" class="but jb-green padding-10" data-forum-draft><i class="fa far fa-save mr6"></i>保存草稿</button>';
|
||||
}
|
||||
$html .= '<button type="submit" class="but jb-blue padding-10 bbs-posts-submit"><i class="fa far fa-paper-plane mr6"></i>' . ($post ? '保存' : '发布') . '</button>';
|
||||
$html .= '</div>';
|
||||
if ($post && $last !== '') {
|
||||
$html .= '<div class="muted-3-color px12"><span class="badg badg-sm mr6">编辑内容</span>最后更新:' . esc_html($last) . '</div>';
|
||||
}
|
||||
return $html . '</div>';
|
||||
}
|
||||
|
||||
private static function status_box(?\WP_Post $post): string {
|
||||
if (!$post instanceof \WP_Post) return '';
|
||||
$status = in_array($post->post_status, ['pending', 'draft', 'publish'], true) ? $post->post_status : 'pending';
|
||||
$list = '';
|
||||
foreach (['pending' => '待审核', 'draft' => '草稿', 'publish' => '发布'] as $key => $label) {
|
||||
$list .= '<span data-forum-status="' . $key . '" class="badg mr3 p2-10 pointer' . ($key === $status ? ' active' : '') . '">' . $label . '</span>';
|
||||
}
|
||||
return '<div class="zib-widget mb10 type-drop"><div class="flex ac jsb"><div class="flex ac c-red"><i class="fa fas fa-thumbtack mr10"></i>设置状态</div><div class="em09">' . $list . '</div></div><input type="hidden" name="post_status" value="' . esc_attr($status) . '"></div>';
|
||||
}
|
||||
|
||||
private static function allow_view_box(string $current): string {
|
||||
$labels = ['all' => '公开', 'login' => '登录后可查看', 'follow' => '关注版块后可查看', 'points' => '支付积分查看', 'pay' => '付费查看'];
|
||||
$options = '';
|
||||
foreach ($labels as $key => $label) {
|
||||
$options .= '<div data-forum-view="' . $key . '" class="pointer but p2-10 mb6' . ($key === $current ? ' active' : '') . '">' . esc_html($label) . '</div>';
|
||||
}
|
||||
return '<div class="allow-view-drop drop-select dropdown zib-widget mb10-sm mb20"><div data-toggle-class="open" data-target=".allow-view-drop" class="flex ac jsb drop-btn"><div class="flex ac"><i class="fa far fa-eye mr6"></i>阅读权限</div><div class="flex ac"><span name="view_limit" class="em09 muted-2-color">' . esc_html($labels[$current] ?? '公开') . '</span><i class="ml6 fa fas fa-angle-right em12"></i></div></div><div class="dropdown-menu fluid"><div class="padding-10 flex jsb muted-2-color em09">设置阅读权限<span data-toggle-class="close" data-target=".allow-view-drop" class="pointer">完成</span></div><div class="mini-scrollbar padding-w10 select-drop-box">' . $options . '</div></div></div>';
|
||||
}
|
||||
|
||||
private static function plate_box(int $current): string {
|
||||
$plates = get_posts(['post_type' => Core::PLATE, 'post_status' => 'publish', 'posts_per_page' => 200, 'orderby' => 'title', 'order' => 'ASC']);
|
||||
$options = '';
|
||||
foreach ($plates as $plate) {
|
||||
$options .= '<div data-forum-plate="' . (int) $plate->ID . '" class="pointer but p2-10 mb6' . ((int) $plate->ID === $current ? ' active' : '') . '">' . esc_html($plate->post_title) . '</div>';
|
||||
}
|
||||
$name = $current > 0 ? get_the_title($current) : '请选择';
|
||||
return '<div class="plate-drop drop-select dropdown zib-widget mb10-sm mb20"><div data-toggle-class="open" data-target=".plate-drop" class="flex ac jsb drop-btn"><div class="flex ac"><i class="fa fas fa-cube mr6"></i>版块</div><div class="flex ac"><span name="plate_id" class="em09 muted-2-color text-ellipsis">' . esc_html($name) . '</span><i class="ml6 fa fas fa-angle-right em12"></i></div></div><input type="hidden" name="plate_id" value="' . ($current ?: '') . '"><div class="dropdown-menu fluid"><div class="mini-scrollbar padding-w10 select-drop-box">' . $options . '</div></div></div>';
|
||||
}
|
||||
|
||||
/** @param string[] $selected */
|
||||
private static function term_box(string $field, string $label, string $taxonomy, array $selected, string $icon): string {
|
||||
$terms = get_terms(['taxonomy' => $taxonomy, 'hide_empty' => false, 'number' => 200, 'orderby' => 'count', 'order' => 'DESC']);
|
||||
$selected = array_values(array_filter(array_map('strval', $selected)));
|
||||
$options = '';
|
||||
foreach (is_array($terms) ? $terms : [] as $term) {
|
||||
$active = in_array($term->name, $selected, true);
|
||||
$options .= '<label class="but p2-10 mb6 pointer' . ($active ? ' active' : '') . '"><input type="checkbox" data-forum-' . esc_attr($field) . ' value="' . esc_attr($term->name) . '"' . checked($active, true, false) . ' hidden> ' . esc_html($term->name) . '</label>';
|
||||
}
|
||||
return '<div class="' . esc_attr($field) . '-drop drop-select dropdown zib-widget mb10-sm mb20"><div data-toggle-class="open" data-target=".' . esc_attr($field) . '-drop" class="flex ac jsb drop-btn"><div class="flex ac"><i class="fa fas ' . esc_attr($icon) . ' mr6"></i>' . esc_html($label) . '</div><div class="flex ac"><span class="em09 muted-2-color text-ellipsis" data-forum-' . esc_attr($field) . '-label>' . esc_html($selected ? implode('、', array_slice($selected, 0, 2)) : '请选择') . '</span><i class="ml6 fa fas fa-angle-right em12"></i></div></div><input type="hidden" name="' . esc_attr($field) . '" value="' . esc_attr(implode(',', $selected)) . '"><div class="dropdown-menu fluid"><div class="mini-scrollbar padding-w10 select-drop-box">' . $options . '</div></div></div>';
|
||||
}
|
||||
|
||||
private static function type_box(string $current): string {
|
||||
$options = [];
|
||||
$options['normal'] = '标准';
|
||||
if (jio_get_setting('jio_forum_question_enabled', true)) $options['question'] = '提问';
|
||||
if (jio_get_setting('jio_forum_vote_enabled', true)) $options['vote'] = '投票';
|
||||
if (count($options) < 2) return '';
|
||||
$list = '';
|
||||
foreach ($options as $key => $label) {
|
||||
$list .= '<span data-forum-type-set="' . $key . '" class="but p2-10 pointer' . ($key === $current ? ' active' : '') . '">' . esc_html($label) . '</span>';
|
||||
}
|
||||
return '<div class="type-drop zib-widget mb10-sm mb20"><div class="flex ac jsb"><div class="flex ac"><i class="fa fas fa-delicious mr6"></i>发布类型</div><div class="but-average radius em09">' . $list . '</div></div></div>';
|
||||
}
|
||||
|
||||
/** @param string[] $options */
|
||||
private static function vote_box(array $options, ?\WP_Post $post): string {
|
||||
$type = $post ? (string) get_post_meta($post->ID, '_jio_forum_type', true) : 'normal';
|
||||
return '<div class="type-drop zib-widget mb10-sm mb20 forum-type-vote"' . ($type === 'vote' ? '' : ' hidden') . '><div class="mb10"><i class="fa fas fa-poll mr6"></i>投票设置</div>'
|
||||
. '<input class="form-control mb6" name="vote_title" maxlength="60" placeholder="投票标题" value="' . esc_attr($post ? (string) get_post_meta($post->ID, '_jio_forum_vote_title', true) : '') . '">'
|
||||
. '<select class="form-control mb6" name="vote_type"><option value="single">单选</option><option value="multiple">多选</option><option value="pk">双选 PK</option></select>'
|
||||
. '<textarea class="form-control mb6" name="vote_options" rows="4" placeholder="每行一个选项">' . esc_textarea(implode("\n", $options)) . '</textarea>'
|
||||
. '<select class="form-control" name="vote_days"><option value="0">永久</option><option value="1">1 天</option><option value="7">7 天</option><option value="30">30 天</option></select></div>';
|
||||
}
|
||||
|
||||
private static function close(): void {
|
||||
echo '</div></div></div></form></main>';
|
||||
get_footer();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
<?php
|
||||
|
||||
namespace Jio\Forum;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/** Forum listing pages: plate header card, plate tab bar and post lists. */
|
||||
final class ListingView {
|
||||
/** Plate page: fluid widgets, blur header card, tab bar, then the list. */
|
||||
public static function plate(): void {
|
||||
$plate = get_queried_object();
|
||||
if (!$plate instanceof \WP_Post || $plate->post_type !== Core::PLATE) {
|
||||
self::term();
|
||||
return;
|
||||
}
|
||||
self::open($plate->post_title, 'plate', $plate);
|
||||
\Jio\WidgetSystem\WidgetAreas::render('forum_plate_top_fluid');
|
||||
echo '<div class="fixed-wrap"><div class="fixed-wrap-content">';
|
||||
echo Cards::plate_header($plate); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
echo self::tab_bar($plate); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
echo '</div></div>';
|
||||
self::close('plate');
|
||||
}
|
||||
|
||||
/** Taxonomy archives (plate category / topic / tag). */
|
||||
public static function term(): void {
|
||||
$term = get_queried_object();
|
||||
if (!$term instanceof \WP_Term) {
|
||||
self::open('论坛', 'term', null);
|
||||
self::close('term');
|
||||
return;
|
||||
}
|
||||
self::open($term->name, 'term', null, $term);
|
||||
\Jio\WidgetSystem\WidgetAreas::render('forum_term_top_fluid');
|
||||
echo '<div class="zib-widget">';
|
||||
echo self::tab_bar(null, $term); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
echo '</div>';
|
||||
self::close('term');
|
||||
}
|
||||
|
||||
private static function open(string $title, string $context, ?\WP_Post $plate, ?\WP_Term $term = null): void {
|
||||
echo '<main id="forum" class="jio-forum forum-listing forum-listing--' . esc_attr($context) . '">';
|
||||
echo '<div class="container"><div class="content-wrap"><div class="content-layout">';
|
||||
echo '<div class="jio-forum__list-main">';
|
||||
if ($plate) {
|
||||
$plate_name = (string) jio_get_setting('jio_forum_plate_name', '版块') ?: '版块';
|
||||
echo '<nav class="jio-forum__breadcrumbs" aria-label="路径"><a href="' . esc_url(home_url('/')) . '">首页</a><span>/</span><a href="' . esc_url(Core::home_url()) . '">论坛</a><span>/</span><span>' . esc_html($title !== '' ? $title : $plate_name) . '</span></nav>';
|
||||
} elseif ($term instanceof \WP_Term) {
|
||||
echo '<div class="zib-widget"><div class="box-body notop"><div class="title-theme">' . esc_html($term->name) . '</div></div>';
|
||||
if ($term->description !== '') echo '<p class="muted-2-color em09 padding-w10">' . esc_html($term->description) . '</p>';
|
||||
echo '</div>';
|
||||
}
|
||||
}
|
||||
|
||||
private static function close(string $context): void {
|
||||
echo '</div>';
|
||||
self::sidebar($context);
|
||||
echo '</div></div>';
|
||||
\Jio\WidgetSystem\WidgetAreas::render('forum_' . $context . '_bottom_fluid');
|
||||
echo '</main>';
|
||||
}
|
||||
|
||||
private static function sidebar(string $context): void {
|
||||
if (!jio_is_show_sidebar()) return;
|
||||
echo '<div class="sidebar">';
|
||||
\Jio\WidgetSystem\WidgetAreas::render('forum_' . $context . '_sidebar');
|
||||
echo '</div>';
|
||||
}
|
||||
|
||||
/** Plate tab bar + panes; the first tab renders server-side, the rest load by `?plate_tab=N`. */
|
||||
public static function tab_bar(?\WP_Post $plate = null, ?\WP_Term $term = null): string {
|
||||
$tabs = self::tabs($plate);
|
||||
if (!$tabs) return '';
|
||||
$active = self::active_index(count($tabs));
|
||||
$base = remove_query_arg(['plate_tab', 'fpage']);
|
||||
if ($term instanceof \WP_Term) $base = get_term_link($term);
|
||||
$nav = $panes = '';
|
||||
foreach ($tabs as $index => $tab) {
|
||||
$id = 'plate-tab-' . $index;
|
||||
$href = add_query_arg('plate_tab', $index, is_string($base) ? $base : Core::home_url());
|
||||
$nav .= '<li' . ($index === $active ? ' class="active"' : '') . '><a href="' . esc_url($href) . '" data-forum-tab="#' . $id . '" data-plate-tab="' . $index . '">' . $tab['title'] . '</a></li>';
|
||||
$panes .= '<div class="forum-tab-pane tab-pane fade' . ($index === $active ? ' in active' : '') . '" id="' . $id . '" data-href="' . esc_url($href) . '">';
|
||||
$panes .= $index === $active
|
||||
? self::pane($tab, $plate, $term, $index)
|
||||
: '<div class="post_ajax_loader">' . HomeTabs::placeholder('posts', 3) . '</div>';
|
||||
$panes .= '</div>';
|
||||
}
|
||||
return '<div class="plate-tab zib-widget"><div class="affix-header-sm" offset-top="7"><ul class="list-inline scroll-x mini-scrollbar tab-nav-theme" role="tablist">' . $nav . '</ul></div><div class="tab-content">' . $panes . '</div></div>';
|
||||
}
|
||||
|
||||
/** @return array<int, array<string, mixed>> */
|
||||
public static function tabs(?\WP_Post $plate = null): array {
|
||||
$tabs = [];
|
||||
$configured = jio_get_setting('jio_forum_plate_tab_items', null);
|
||||
if (is_array($configured) && $configured) {
|
||||
foreach ($configured as $row) {
|
||||
if (!is_array($row)) continue;
|
||||
$tabs[] = [
|
||||
'title' => wp_kses_post((string) ($row['title'] ?? '') ?: '栏目'),
|
||||
'orderby' => sanitize_key((string) ($row['orderby'] ?? 'date')),
|
||||
'filter' => sanitize_key((string) ($row['filter'] ?? '')),
|
||||
'style' => sanitize_key((string) ($row['style'] ?? 'detail')),
|
||||
];
|
||||
}
|
||||
} else {
|
||||
foreach ([
|
||||
['最新发布', 'date', '', 'detail'],
|
||||
['最近回复', 'last_reply', '', 'detail'],
|
||||
['热门', 'views', '', 'detail'],
|
||||
['精华', 'modified', 'essence', 'detail'],
|
||||
] as [$title, $orderby, $filter, $style]) {
|
||||
$tabs[] = ['title' => $title, 'orderby' => $orderby, 'filter' => $filter, 'style' => $style];
|
||||
}
|
||||
}
|
||||
$user_id = get_current_user_id();
|
||||
if ($plate instanceof \WP_Post && Access::is_moderator($user_id, $plate->ID)) {
|
||||
$tabs[] = ['title' => '待审核', 'orderby' => 'date', 'filter' => '', 'style' => 'mini', 'status' => 'pending'];
|
||||
$tabs[] = ['title' => '<i class="fa fas fa-trash-alt mr3"></i>回收站', 'orderby' => 'date', 'filter' => '', 'style' => 'mini', 'status' => 'trash'];
|
||||
}
|
||||
return $tabs;
|
||||
}
|
||||
|
||||
public static function active_index(int $count): int {
|
||||
$raw = $_GET['plate_tab'] ?? 0;
|
||||
$index = (is_scalar($raw) ? absint($raw) : 0) ?: (int) jio_get_setting('jio_forum_plate_tab_active', 1);
|
||||
return $index >= 1 && $index <= $count ? $index : 1;
|
||||
}
|
||||
|
||||
private static function pane(array $tab, ?\WP_Post $plate, ?\WP_Term $term, int $index): string {
|
||||
$page = max(1, is_scalar($_GET['fpage'] ?? 1) ? absint($_GET['fpage'] ?? 1) : 1);
|
||||
$args = [
|
||||
'post_type' => Core::POST,
|
||||
'post_status' => $tab['status'] ?? 'publish',
|
||||
'posts_per_page' => max(1, min(50, (int) jio_get_setting('jio_forum_page_size', 20))),
|
||||
'paged' => $page,
|
||||
'jio_forum_list' => $tab['orderby'],
|
||||
];
|
||||
if ($plate instanceof \WP_Post) {
|
||||
$args['meta_query'][] = ['key' => '_jio_forum_plate_id', 'value' => $plate->ID];
|
||||
$topping = self::topping($plate->ID, $tab);
|
||||
if ($topping) {
|
||||
$args['post__not_in'] = $topping;
|
||||
}
|
||||
} elseif ($term instanceof \WP_Term) {
|
||||
$args['tax_query'][] = ['taxonomy' => $term->taxonomy, 'terms' => [$term->term_id]];
|
||||
}
|
||||
if (($tab['filter'] ?? '') === 'essence') {
|
||||
$args['meta_query'][] = ['key' => '_jio_forum_essence', 'value' => 1];
|
||||
}
|
||||
$query = new \WP_Query($args);
|
||||
$html = '';
|
||||
if ($plate instanceof \WP_Post && ($tab['status'] ?? '') === '') {
|
||||
Cards::prime($topping_posts = self::topping_posts($plate->ID, $tab));
|
||||
foreach ($topping_posts as $post) {
|
||||
$html .= Cards::post_mini($post, 'ajax-item', true);
|
||||
}
|
||||
}
|
||||
if ($query->have_posts()) {
|
||||
Cards::prime($query->posts);
|
||||
foreach ($query->posts as $post) {
|
||||
$html .= ($tab['style'] ?? 'detail') === 'detail'
|
||||
? Cards::post_detail($post, 'ajax-item')
|
||||
: Cards::post_mini($post, 'ajax-item');
|
||||
}
|
||||
}
|
||||
$base = $plate instanceof \WP_Post ? get_permalink($plate) : ($term instanceof \WP_Term ? get_term_link($term) : Core::home_url());
|
||||
$next = add_query_arg(['plate_tab' => $index, 'fpage' => $page + 1], is_string($base) ? $base : Core::home_url());
|
||||
if ($page < (int) $query->max_num_pages) {
|
||||
$html .= '<div class="text-center theme-pagination forum-more"><a class="but hollow c-blue" href="' . esc_url($next) . '" data-forum-more>加载更多</a></div>';
|
||||
}
|
||||
$html .= '<div class="post_ajax_loader">' . HomeTabs::placeholder('posts', 3) . '</div>';
|
||||
if ($html !== '' && trim(wp_strip_all_tags($html)) !== '') return $html;
|
||||
$empty = jio_get_ajax_null('内容空空如也');
|
||||
if ($index === 1 && $page === 1 && $plate instanceof \WP_Post) {
|
||||
$empty .= '<div class="text-center mb20"><a class="ml10 mr10 mt10 but hollow c-green padding-lg" href="' . esc_url(Core::edit_url()) . '"><i class="fa fas fa-plus mr3"></i>发布' . esc_html((string) jio_get_setting('jio_forum_post_name', '帖子')) . '</a></div>';
|
||||
}
|
||||
return $empty;
|
||||
}
|
||||
|
||||
/** @return int[] */
|
||||
private static function topping(int $plate_id, array $tab): array {
|
||||
if (($tab['filter'] ?? '') === 'essence' || ($tab['status'] ?? '') !== '') return [];
|
||||
return wp_list_pluck(self::topping_posts($plate_id, $tab), 'ID');
|
||||
}
|
||||
|
||||
/** @return \WP_Post[] */
|
||||
private static function topping_posts(int $plate_id, array $tab): array {
|
||||
return get_posts([
|
||||
'post_type' => Core::POST,
|
||||
'post_status' => 'publish',
|
||||
'posts_per_page' => 5,
|
||||
'meta_query' => [
|
||||
['key' => '_jio_forum_plate_id', 'value' => $plate_id],
|
||||
['key' => '_jio_forum_pinned', 'value' => 1],
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
<?php
|
||||
|
||||
namespace Jio\Forum;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/** Forum post detail page (reference article layout). */
|
||||
final class PostView {
|
||||
public static function render(): void {
|
||||
$post = get_post();
|
||||
if (!$post instanceof \WP_Post || $post->post_type !== Core::POST) {
|
||||
return;
|
||||
}
|
||||
$id = (int) $post->ID;
|
||||
$plate_id = (int) get_post_meta($id, '_jio_forum_plate_id', true);
|
||||
$type = (string) get_post_meta($id, '_jio_forum_type', true);
|
||||
$user_id = get_current_user_id();
|
||||
$can_view = Access::can_view($user_id, $id);
|
||||
|
||||
echo '<main id="forum" class="jio-forum forum-post"><div class="container"><div class="content-wrap"><div class="content-layout">';
|
||||
echo '<div class="fixed-wrap single-wrap"><div class="fixed-wrap-content">';
|
||||
\Jio\WidgetSystem\WidgetAreas::render('forum_post_top_fluid');
|
||||
do_action('bbs_posts_page_content_top');
|
||||
if ($can_view) echo self::cover($post);
|
||||
echo self::breadcrumbs($post);
|
||||
echo '<article id="article" class="article zib-widget forum-article relative">';
|
||||
echo '<div class="article-header clearfix">' . self::title($post) . self::meta($post) . '</div>';
|
||||
echo self::content($post, $can_view);
|
||||
echo self::footer($post);
|
||||
echo '</article>';
|
||||
if ($can_view) {
|
||||
self::comments($id);
|
||||
}
|
||||
do_action('bbs_posts_page_content_bottom');
|
||||
\Jio\WidgetSystem\WidgetAreas::render('forum_post_bottom_fluid');
|
||||
echo '</div></div></div>';
|
||||
if (jio_is_show_sidebar()) {
|
||||
echo '<div class="sidebar">';
|
||||
\Jio\WidgetSystem\WidgetAreas::render('forum_post_sidebar');
|
||||
echo '</div>';
|
||||
}
|
||||
echo '</div></div></main>';
|
||||
}
|
||||
|
||||
private static function breadcrumbs(\WP_Post $post): string {
|
||||
$plate_id = (int) get_post_meta($post->ID, '_jio_forum_plate_id', true);
|
||||
$icon = '<i class="fa fas fa-map-marker-alt"></i> ';
|
||||
$html = '<ul class="breadcrumb">';
|
||||
$html .= '<li><a href="' . esc_url(home_url('/')) . '">' . $icon . '首页</a></li>';
|
||||
$html .= '<li><a href="' . esc_url(Core::home_url()) . '">论坛</a></li>';
|
||||
if ($plate_id > 0) {
|
||||
$terms = wp_get_post_terms($plate_id, Core::CATEGORY);
|
||||
if (is_array($terms) && $terms) {
|
||||
$html .= '<li><a href="' . esc_url(get_term_link($terms[0])) . '">' . esc_html($terms[0]->name) . '</a></li>';
|
||||
}
|
||||
$html .= '<li><a href="' . esc_url(get_permalink($plate_id)) . '">' . esc_html(get_the_title($plate_id)) . '</a></li>';
|
||||
}
|
||||
$html .= '<li class="active">' . esc_html(wp_trim_words($post->post_title, 8)) . '</li>';
|
||||
return $html . '</ul>';
|
||||
}
|
||||
|
||||
private static function cover(\WP_Post $post): string {
|
||||
if (!has_post_thumbnail($post)) return '';
|
||||
return '<div class="forum-single-cover mb20"><div class="graphic imgbox-container" style="padding-bottom:45%">' . get_the_post_thumbnail($post, 'large', ['class' => 'fit-cover']) . '</div></div>';
|
||||
}
|
||||
|
||||
private static function title(\WP_Post $post): string {
|
||||
return Cards::title($post, 'article-title', '', true);
|
||||
}
|
||||
|
||||
private static function meta(\WP_Post $post): string {
|
||||
$author = (int) $post->post_author;
|
||||
$name = function_exists('jio_get_user_name') ? jio_get_user_name(['id' => $author, 'class' => '', 'name_class' => 'focus-color']) : esc_html((string) get_the_author_meta('display_name', $author));
|
||||
$left = '<div class="meta-left">' . $name
|
||||
. '<span class="icon-spot"><span title="' . esc_attr(get_the_date('Y-m-d H:i:s', $post)) . '">' . esc_html(Cards::ago($post->post_date)) . '</span></span></div>';
|
||||
$right = '<div class="meta-right">'
|
||||
. '<item class="item item-view"><i class="fa far fa-eye mr3"></i>' . esc_html(jio_cut_count((int) get_post_meta($post->ID, '_jio_forum_views', true))) . '</item>'
|
||||
. '<a class="item item-comment" href="#comments"><i class="fa far fa-comment mr3"></i>' . esc_html(jio_cut_count((int) $post->comment_count)) . '</a>'
|
||||
. '</div>';
|
||||
return '<div class="flex ac jsb forum-article-meta">' . $left . $right . '</div>';
|
||||
}
|
||||
|
||||
private static function content(\WP_Post $post, bool $can_view): string {
|
||||
$id = (int) $post->ID;
|
||||
if (!$can_view) {
|
||||
return '<div class="theme-box wp-posts-content"><div class="theme-box">' . self::locked($post) . '</div></div>';
|
||||
}
|
||||
$html = '<div class="theme-box wp-posts-content">' . apply_filters('the_content', $post->post_content) . '</div>';
|
||||
$type = (string) get_post_meta($id, '_jio_forum_type', true);
|
||||
if ($type === 'vote') {
|
||||
$html .= self::vote($post);
|
||||
}
|
||||
return $html;
|
||||
}
|
||||
|
||||
private static function locked(\WP_Post $post): string {
|
||||
$id = (int) $post->ID;
|
||||
$plate_id = (int) get_post_meta($id, '_jio_forum_plate_id', true);
|
||||
$user_id = get_current_user_id();
|
||||
$html = jio_get_null('抱歉!您暂无查看此内容的权限', 10, 'null-post.svg');
|
||||
if ($user_id <= 0) {
|
||||
return $html . '<p class="text-center"><a class="but jb-blue padding-lg" href="' . esc_url(jio_get_login_url(get_permalink($post))) . '"><i class="fa fas fa-sign-in-alt mr6"></i>登录后查看</a></p>';
|
||||
}
|
||||
$plate_rule = (string) get_post_meta($plate_id, '_jio_forum_view_limit', true);
|
||||
$post_rule = (string) get_post_meta($id, '_jio_forum_view_limit', true);
|
||||
$action = '';
|
||||
if (in_array($plate_rule, ['points', 'pay'], true) && !Commerce::has_plate_entitlement($user_id, $plate_id)) {
|
||||
$action = '<button type="button" class="but jb-blue padding-lg" data-forum-action="plate-unlock" data-id="' . $plate_id . '"><i class="fa fas fa-unlock-alt mr6"></i>解锁版块</button>';
|
||||
} elseif (($plate_rule === 'follow' || $post_rule === 'follow') && !Access::follows($user_id, $plate_id)) {
|
||||
$action = Cards::follow_button($plate_id, 'but jb-blue padding-lg');
|
||||
} elseif (in_array($post_rule, ['points', 'pay'], true)) {
|
||||
$action = '<button type="button" class="but jb-blue padding-lg" data-forum-action="unlock" data-id="' . $id . '"><i class="fa fas fa-unlock-alt mr6"></i>解锁帖子</button>';
|
||||
}
|
||||
return $html . ($action !== '' ? '<p class="text-center">' . $action . '</p>' : '');
|
||||
}
|
||||
|
||||
private static function vote(\WP_Post $post): string {
|
||||
$id = (int) $post->ID;
|
||||
$options = (array) get_post_meta($id, '_jio_forum_vote_options', true);
|
||||
if (!$options) return '';
|
||||
$counts = (array) get_post_meta($id, '_jio_forum_vote_counts', true);
|
||||
$total = (int) get_post_meta($id, '_jio_forum_vote_total', true);
|
||||
$vote_type = (string) get_post_meta($id, '_jio_forum_vote_type', true);
|
||||
$deadline = (int) get_post_meta($id, '_jio_forum_vote_deadline', true);
|
||||
$expired = $deadline > 0 && current_time('timestamp') > $deadline;
|
||||
$display = (string) jio_get_setting('jio_forum_vote_number_type', 'percentage');
|
||||
$voted = get_current_user_id() > 0 && get_user_meta(get_current_user_id(), '_jio_forum_vote_' . $id, true) !== '';
|
||||
$html = '<form class="vote-box forum-vote zib-widget" data-forum-form="vote" data-id="' . $id . '"><h2 class="mb20">' . esc_html((string) get_post_meta($id, '_jio_forum_vote_title', true) ?: '参与投票') . '</h2>';
|
||||
foreach ($options as $index => $option) {
|
||||
$count = (int) ($counts[$index] ?? 0);
|
||||
$percent = $total > 0 ? (int) round(100 * $count / $total) : 0;
|
||||
$html .= '<label class="vote-item"><input type="' . ($vote_type === 'multiple' ? 'checkbox' : 'radio') . '" name="' . ($vote_type === 'multiple' ? 'option[]' : 'option') . '" value="' . (int) $index . '"' . disabled($voted || $expired, true, false) . '><span class="vote-title">' . esc_html((string) $option) . '</span>';
|
||||
if ($display !== 'hide') {
|
||||
$html .= '<span class="vote-count em09 muted-2-color">' . esc_html($display === 'number' ? (string) $count : $percent . '%') . '</span>';
|
||||
}
|
||||
$html .= '<span class="vote-progress" style="width:' . $percent . '%"></span></label>';
|
||||
}
|
||||
$html .= '<p class="jio-forum__message" role="status"></p>';
|
||||
if ($voted) $html .= '<p class="text-center muted-2-color">已投票</p>';
|
||||
elseif ($expired) $html .= '<p class="text-center muted-2-color">投票已截止</p>';
|
||||
else $html .= '<button type="submit" class="but jb-blue padding-lg"><i class="fa fas fa-check mr6"></i>提交投票</button>';
|
||||
return $html . '</form>';
|
||||
}
|
||||
|
||||
private static function footer(\WP_Post $post): string {
|
||||
$id = (int) $post->ID;
|
||||
$user_id = get_current_user_id();
|
||||
$plate_id = (int) get_post_meta($id, '_jio_forum_plate_id', true);
|
||||
$html = '<div class="article-tags mb20 em09">';
|
||||
foreach (is_array($terms = wp_get_post_terms($id, Core::TOPIC)) ? $terms : [] as $term) {
|
||||
$html .= '<a class="but radius mm3 c-red" href="' . esc_url(get_term_link($term)) . '"><i class="fa fas fa-hashtag mr6"></i>' . esc_html($term->name) . '</a>';
|
||||
}
|
||||
foreach (is_array($tags = wp_get_post_terms($id, Core::TAG)) ? $tags : [] as $term) {
|
||||
$html .= '<a class="but radius mm3" href="' . esc_url(get_term_link($term)) . '"><i class="fa fas fa-tag mr6"></i>' . esc_html($term->name) . '</a>';
|
||||
}
|
||||
$html .= '</div>';
|
||||
$html .= '<div class="forum-article-footer flex ac jsb mb10 footer-actions"><div class="left flex ac">';
|
||||
if (jio_get_setting('jio_forum_score_enabled', true)) {
|
||||
$score = (int) get_post_meta($id, '_jio_forum_score', true);
|
||||
$html .= '<button type="button" class="item btn-score" data-forum-action="score" data-direction="up" data-id="' . $id . '"><i class="fa far fa-thumbs-up"></i><text>' . esc_html(jio_cut_count($score)) . '</text></button>';
|
||||
$html .= '<button type="button" class="item btn-score" data-forum-action="score" data-direction="down" data-id="' . $id . '"><i class="fa far fa-thumbs-down"></i></button>';
|
||||
}
|
||||
$html .= '</div><div class="right flex ac">';
|
||||
if (function_exists('jio_get_post_share_btn')) $html .= jio_get_post_share_btn($post, 'item', true, false);
|
||||
$html .= '<button type="button" class="item" data-forum-action="favorite" data-id="' . $id . '"><i class="fa far fa-bookmark"></i><text>收藏</text></button>';
|
||||
$html .= '<button type="button" class="item" data-forum-action="report" data-id="' . $id . '"><i class="fa far fa-flag"></i><text>举报</text></button>';
|
||||
if (Access::can_manage_post($user_id, $id)) {
|
||||
$html .= '<a class="item" href="' . esc_url(Core::edit_url($id)) . '"><i class="fa far fa-edit"></i><text>编辑</text></a>';
|
||||
$html .= '<button type="button" class="item" data-forum-action="delete" data-id="' . $id . '"><i class="fa far fa-trash-alt"></i><text>删除</text></button>';
|
||||
}
|
||||
$html .= '</div></div>';
|
||||
$html .= '<div class="single-footer text-center mb20">';
|
||||
if (Access::is_moderator($user_id, $plate_id)) {
|
||||
foreach ([['essence', '精华'], ['pin', '置顶'], ['audit', '审核'], ['comments', $post->comment_status === 'open' ? '关闭回复' : '开放回复'], ['move', '移动版块']] as [$action, $label]) {
|
||||
$html .= '<button type="button" class="but hollow px12 mr6" data-forum-action="' . $action . '" data-id="' . $id . '">' . esc_html($label) . '</button>';
|
||||
}
|
||||
}
|
||||
$html .= '</div>';
|
||||
if (get_post_meta($id, '_jio_forum_type', true) === 'question' && get_post_meta($id, '_jio_forum_adopted_comment', true)) {
|
||||
$html .= '<p class="muted-2-color em09 mb20"><i class="fa fas fa-check-circle c-green mr6"></i>已采纳回复</p>';
|
||||
}
|
||||
return $html;
|
||||
}
|
||||
|
||||
private static function comments(int $post_id): void {
|
||||
echo '<div class="theme-box box-body radius8 main-shadow comments-card mb20">';
|
||||
View::replies($post_id);
|
||||
echo '</div>';
|
||||
}
|
||||
}
|
||||
@@ -107,6 +107,13 @@ trait RestPosts {
|
||||
return self::fail('已有采纳回复后不能更改帖子类型');
|
||||
}
|
||||
$status = Access::is_moderator($user_id, $plate_id) || !jio_get_setting('jio_forum_post_review', true) ? 'publish' : 'pending';
|
||||
// Authors may park their own post as a draft from the compose page.
|
||||
$requested = sanitize_key((string) ($input['post_status'] ?? ''));
|
||||
if ($requested === 'draft' && !($current && in_array($current->post_status, ['publish', 'pending'], true))) {
|
||||
$status = 'draft';
|
||||
} elseif ($requested !== '' && $current && Access::is_moderator($user_id, $plate_id) && in_array($requested, ['publish', 'pending', 'draft'], true)) {
|
||||
$status = $requested;
|
||||
}
|
||||
$post_data = [
|
||||
'ID' => $id, 'post_type' => Core::POST, 'post_title' => $title, 'post_content' => $content,
|
||||
'post_status' => $status, 'post_author' => $current ? (int) $current->post_author : $user_id,
|
||||
@@ -124,6 +131,12 @@ trait RestPosts {
|
||||
update_post_meta($id, '_jio_forum_type', $type);
|
||||
update_post_meta($id, '_jio_forum_view_limit', $view);
|
||||
update_post_meta($id, '_jio_forum_price', $price);
|
||||
$cover_id = absint($input['featured_image_id'] ?? 0);
|
||||
if ($cover_id > 0 && wp_attachment_is_image($cover_id)) {
|
||||
set_post_thumbnail($id, $cover_id);
|
||||
} elseif (array_key_exists('featured_image_id', $input)) {
|
||||
delete_post_thumbnail($id);
|
||||
}
|
||||
foreach (['vip1', 'vip2'] as $vip) {
|
||||
if (!isset($input['price_' . $vip])) continue;
|
||||
$vip_price = (string) $input['price_' . $vip];
|
||||
|
||||
+1
-61
@@ -44,67 +44,7 @@ final class View {
|
||||
}
|
||||
|
||||
public static function listing(): void {
|
||||
$obj = get_queried_object();
|
||||
$plate = $obj instanceof \WP_Post && $obj->post_type === Core::PLATE ? $obj : null;
|
||||
$term = $obj instanceof \WP_Term ? $obj : null;
|
||||
$title = $plate ? $plate->post_title : ($term ? $term->name : '论坛');
|
||||
$context = $plate ? 'plate' : 'term';
|
||||
self::open($title, $context);
|
||||
\Jio\WidgetSystem\WidgetAreas::render('forum_' . $context . '_top_fluid');
|
||||
echo '<div class="jio-forum__layout"><div class="jio-forum__main">';
|
||||
echo '<header class="jio-forum__page-head"><h1>' . esc_html($title) . '</h1>';
|
||||
if ($plate) {
|
||||
echo '<div class="jio-forum__description">' . wp_kses_post(wpautop($plate->post_excerpt ?: $plate->post_content)) . '</div>';
|
||||
echo '<p class="jio-forum__stats">' . (int) get_post_meta($plate->ID, '_jio_forum_follow_count', true) . ' 人关注</p>';
|
||||
echo Cards::follow_button($plate->ID, 'but c-blue'); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
if (Access::is_moderator(get_current_user_id(), $plate->ID)) {
|
||||
self::plate_editor($plate);
|
||||
}
|
||||
if (is_user_logged_in() && !Access::is_moderator(get_current_user_id(), $plate->ID) && jio_get_setting('jio_forum_moderator_apply', true)) {
|
||||
self::action('apply', $plate->ID, '申请版主');
|
||||
}
|
||||
if (Access::is_admin(get_current_user_id()) || (int) $plate->post_author === get_current_user_id()) {
|
||||
self::applications($plate->ID);
|
||||
}
|
||||
} elseif ($term) {
|
||||
echo '<p>' . esc_html($term->description) . '</p>';
|
||||
}
|
||||
echo '</header>';
|
||||
self::tabs();
|
||||
if ($term && $term->taxonomy === Core::CATEGORY) {
|
||||
self::plates(['tax_query' => [['taxonomy' => Core::CATEGORY, 'terms' => [$term->term_id]]]]);
|
||||
} else {
|
||||
$args = ['post_type' => Core::POST, 'post_status' => 'publish'];
|
||||
if ($plate) {
|
||||
$args['meta_query'] = [['key' => '_jio_forum_plate_id', 'value' => $plate->ID]];
|
||||
} elseif ($term) {
|
||||
$args['tax_query'] = [['taxonomy' => $term->taxonomy, 'terms' => [$term->term_id]]];
|
||||
}
|
||||
self::posts($args, $context);
|
||||
if ($plate && Access::is_moderator(get_current_user_id(), $plate->ID)) {
|
||||
echo '<h2 class="jio-forum__section-title">待审核帖子</h2>';
|
||||
$args['post_status'] = 'pending';
|
||||
self::posts($args, $context);
|
||||
}
|
||||
}
|
||||
echo '</div>';
|
||||
self::sidebar($context);
|
||||
echo '</div>';
|
||||
\Jio\WidgetSystem\WidgetAreas::render('forum_' . $context . '_bottom_fluid');
|
||||
self::close();
|
||||
}
|
||||
|
||||
/** Listing pages (plate / taxonomy) use a fixed, always-available tab set. */
|
||||
public static function tabs(): void {
|
||||
$tabs = ['latest' => '最新', 'popular' => '热门', 'essence' => '精华', 'question' => '问答', 'vote' => '投票'];
|
||||
$base_url = is_singular(Core::PLATE) ? get_permalink(get_queried_object_id()) : (is_tax([Core::CATEGORY, Core::TOPIC, Core::TAG]) ? get_term_link(get_queried_object()) : Core::home_url());
|
||||
echo '<nav class="jio-forum__tabs" aria-label="论坛栏目">';
|
||||
foreach ($tabs as $key => $label) {
|
||||
$url = add_query_arg('forum_view', $key, $base_url);
|
||||
$selected = (string) ($_GET['forum_view'] ?? 'latest') === $key;
|
||||
echo '<a class="' . ($selected ? 'is-active' : '') . '" href="' . esc_url($url) . '">' . esc_html($label) . '</a>';
|
||||
}
|
||||
echo '</nav>';
|
||||
ListingView::plate();
|
||||
}
|
||||
|
||||
public static function posts(array $args, string $context = 'home'): void {
|
||||
|
||||
@@ -1,57 +1,4 @@
|
||||
<?php
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
use Jio\Forum\Access;
|
||||
use Jio\Forum\Core;
|
||||
use Jio\Forum\View;
|
||||
|
||||
$edit_id = absint(get_query_var('jio_forum_edit'));
|
||||
$post = $edit_id > 0 ? get_post($edit_id) : null;
|
||||
if (!is_user_logged_in()) {
|
||||
wp_safe_redirect(jio_get_login_url(Core::edit_url($edit_id)));
|
||||
exit;
|
||||
}
|
||||
if ($edit_id > 0 && (!$post instanceof WP_Post || !Access::can_manage_post(get_current_user_id(), $edit_id))) {
|
||||
status_header(403);
|
||||
wp_die(esc_html__('无权编辑此帖子', 'jio'));
|
||||
}
|
||||
|
||||
wp_enqueue_editor();
|
||||
|
||||
$plate_id = $post ? (int) get_post_meta($post->ID, '_jio_forum_plate_id', true) : absint($_GET['plate'] ?? 0);
|
||||
$vote_started = $post ? (int) get_post_meta($post->ID, '_jio_forum_vote_started', true) : 0;
|
||||
$vote_deadline = $post ? (int) get_post_meta($post->ID, '_jio_forum_vote_deadline', true) : 0;
|
||||
$vote_days = $vote_started && $vote_deadline ? (int) round(($vote_deadline - $vote_started) / DAY_IN_SECONDS) : 0;
|
||||
$plates = get_posts(['post_type' => Core::PLATE, 'post_status' => 'publish', 'posts_per_page' => -1, 'orderby' => 'title', 'order' => 'ASC']);
|
||||
View::open($post ? '编辑帖子' : '发布帖子', 'edit');
|
||||
?>
|
||||
<div class="jio-forum__layout"><div class="jio-forum__main">
|
||||
<h1><?php echo $post ? '编辑帖子' : '发布帖子'; ?></h1>
|
||||
<form class="jio-forum__form" data-forum-form="post" data-id="<?php echo esc_attr((string) $edit_id); ?>">
|
||||
<label>标题<input name="title" required maxlength="120" value="<?php echo esc_attr($post ? $post->post_title : ''); ?>"></label>
|
||||
<label>版块<select name="plate_id" required><option value="">选择版块</option><?php foreach ($plates as $plate) : ?><option value="<?php echo esc_attr((string) $plate->ID); ?>" <?php selected($plate_id, $plate->ID); ?>><?php echo esc_html($plate->post_title); ?></option><?php endforeach; ?></select></label>
|
||||
<label>类型<select name="type"><option value="normal">普通帖子</option><option value="question" <?php selected($post ? get_post_meta($post->ID, '_jio_forum_type', true) : '', 'question'); ?>>问答</option><option value="vote" <?php selected($post ? get_post_meta($post->ID, '_jio_forum_type', true) : '', 'vote'); ?>>投票</option></select></label>
|
||||
<label>正文</label>
|
||||
<?php wp_editor($post ? $post->post_content : '', 'jio_forum_content', ['textarea_name' => 'content', 'textarea_rows' => 16, 'media_buttons' => false, 'quicktags' => true, 'tinymce' => ['toolbar1' => 'formatselect,bold,italic,bullist,numlist,blockquote,link,unlink,alignleft,aligncenter,alignright,undo,redo', 'toolbar2' => 'forecolor,removeformat']]); ?>
|
||||
<div class="jio-forum__editor-tools">
|
||||
<?php foreach (['image' => '插入图片', 'video' => '插入视频', 'resource' => '插入附件'] as $kind => $label) : ?>
|
||||
<?php if (jio_get_setting('jio_forum_upload_' . ($kind === 'resource' ? 'file' : $kind), $kind === 'image')) : ?><button type="button" class="jio-forum__action" data-forum-upload="<?php echo esc_attr($kind); ?>"><?php echo esc_html($label); ?></button><?php endif; ?>
|
||||
<?php endforeach; ?>
|
||||
<input type="file" data-forum-file hidden>
|
||||
</div>
|
||||
<label>话题<input name="topics" value="<?php echo esc_attr($post ? implode(', ', wp_get_post_terms($post->ID, Core::TOPIC, ['fields' => 'names'])) : ''); ?>" placeholder="多个话题以逗号分隔"></label>
|
||||
<label>标签<input name="tags" value="<?php echo esc_attr($post ? implode(', ', wp_get_post_terms($post->ID, Core::TAG, ['fields' => 'names'])) : ''); ?>" placeholder="多个标签以逗号分隔"></label>
|
||||
<div class="jio-forum__form-extra" data-forum-type="vote">
|
||||
<label>投票标题<input name="vote_title" maxlength="60" value="<?php echo esc_attr($post ? (string) get_post_meta($post->ID, '_jio_forum_vote_title', true) : ''); ?>"></label>
|
||||
<label>投票方式<select name="vote_type"><option value="single">单选</option><option value="multiple" <?php selected($post ? get_post_meta($post->ID, '_jio_forum_vote_type', true) : '', 'multiple'); ?>>多选</option><option value="pk" <?php selected($post ? get_post_meta($post->ID, '_jio_forum_vote_type', true) : '', 'pk'); ?>>双选 PK</option></select></label>
|
||||
<label>投票选项<textarea name="vote_options" rows="4" placeholder="每行一个选项"><?php echo esc_textarea($post ? implode("\n", (array) get_post_meta($post->ID, '_jio_forum_vote_options', true)) : ''); ?></textarea></label>
|
||||
<label>有效期<select name="vote_days"><option value="0" <?php selected($vote_days, 0); ?>>永久</option><option value="1" <?php selected($vote_days, 1); ?>>1 天</option><option value="7" <?php selected($vote_days, 7); ?>>7 天</option><option value="30" <?php selected($vote_days, 30); ?>>30 天</option></select></label>
|
||||
</div>
|
||||
<label>访问权限<select name="view_limit"><option value="all">所有人</option><option value="login" <?php selected($post ? get_post_meta($post->ID, '_jio_forum_view_limit', true) : '', 'login'); ?>>登录后</option><option value="follow" <?php selected($post ? get_post_meta($post->ID, '_jio_forum_view_limit', true) : '', 'follow'); ?>>关注版块后</option><option value="points" <?php selected($post ? get_post_meta($post->ID, '_jio_forum_view_limit', true) : '', 'points'); ?>>积分解锁</option><option value="pay" <?php selected($post ? get_post_meta($post->ID, '_jio_forum_view_limit', true) : '', 'pay'); ?>>付费解锁</option></select></label>
|
||||
<label>解锁价格<input name="price" type="number" min="0" step="0.01" value="<?php echo esc_attr($post ? (string) get_post_meta($post->ID, '_jio_forum_price', true) : '0'); ?>"></label>
|
||||
<label>会员一级价格<input name="price_vip1" type="number" min="0" step="0.01" value="<?php echo esc_attr($post ? (string) get_post_meta($post->ID, '_jio_forum_price_vip1', true) : ''); ?>"></label>
|
||||
<label>会员二级价格<input name="price_vip2" type="number" min="0" step="0.01" value="<?php echo esc_attr($post ? (string) get_post_meta($post->ID, '_jio_forum_price_vip2', true) : ''); ?>"></label>
|
||||
<p class="jio-forum__message" role="status" aria-live="polite"></p><button type="submit" class="jio-forum__primary">保存帖子</button>
|
||||
</form>
|
||||
</div><?php View::sidebar('edit'); ?></div>
|
||||
<?php View::close(); ?>
|
||||
\Jio\Forum\EditView::render();
|
||||
@@ -1,85 +1,5 @@
|
||||
<?php
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
use Jio\Forum\Access;
|
||||
use Jio\Forum\Core;
|
||||
use Jio\Forum\View;
|
||||
|
||||
the_post();
|
||||
$post = get_post();
|
||||
$id = (int) $post->ID;
|
||||
$plate_id = (int) get_post_meta($id, '_jio_forum_plate_id', true);
|
||||
$type = (string) get_post_meta($id, '_jio_forum_type', true);
|
||||
$user_id = get_current_user_id();
|
||||
$can_view = Access::can_view($user_id, $id);
|
||||
$can_manage = Access::can_manage_post($user_id, $id);
|
||||
$can_moderate = Access::is_moderator($user_id, $plate_id);
|
||||
View::open($post->post_title, 'post');
|
||||
\Jio\WidgetSystem\WidgetAreas::render('forum_post_top_fluid');
|
||||
?>
|
||||
<div class="jio-forum__layout"><div class="jio-forum__main">
|
||||
<article class="jio-forum__single">
|
||||
<header class="jio-forum__single-head">
|
||||
<div class="jio-forum__single-meta"><?php echo get_avatar((int) $post->post_author, 40); ?><span><?php echo esc_html(get_the_author_meta('display_name', (int) $post->post_author)); ?></span><time datetime="<?php echo esc_attr(get_the_date(DATE_W3C)); ?>"><?php echo esc_html(get_the_date()); ?></time></div>
|
||||
<h1><?php echo esc_html($post->post_title); ?></h1>
|
||||
<div class="jio-forum__badges">
|
||||
<?php if (get_post_meta($id, '_jio_forum_pinned', true)) : ?><span>置顶</span><?php endif; ?>
|
||||
<?php if ($type === 'question') : ?><span>问答</span><?php elseif ($type === 'vote') : ?><span>投票</span><?php endif; ?>
|
||||
<?php if ($type === 'question' && get_post_meta($id, '_jio_forum_adopted_comment', true)) : ?><span>已解决</span><?php endif; ?>
|
||||
<?php if (get_post_meta($id, '_jio_forum_essence', true)) : ?><span>精华</span><?php endif; ?>
|
||||
<?php if ($plate_id > 0) : ?><a href="<?php echo esc_url(get_permalink($plate_id)); ?>"><?php echo esc_html(get_the_title($plate_id)); ?></a><?php endif; ?>
|
||||
<?php foreach (wp_get_post_terms($id, Core::TOPIC) as $term) : ?><a href="<?php echo esc_url(get_term_link($term)); ?>">#<?php echo esc_html($term->name); ?></a><?php endforeach; ?>
|
||||
</div>
|
||||
</header>
|
||||
<?php if ($can_view) : ?>
|
||||
<div class="jio-forum__single-content"><?php echo apply_filters('the_content', $post->post_content); ?></div>
|
||||
<?php if ($type === 'vote') : ?>
|
||||
<?php
|
||||
$options = (array) get_post_meta($id, '_jio_forum_vote_options', true);
|
||||
$votes = (array) get_post_meta($id, '_jio_forum_vote_counts', true);
|
||||
$vote_type = (string) get_post_meta($id, '_jio_forum_vote_type', true);
|
||||
$deadline = (int) get_post_meta($id, '_jio_forum_vote_deadline', true);
|
||||
$expired = $deadline > 0 && current_time('timestamp') > $deadline;
|
||||
$vote_display = (string) jio_get_setting('jio_forum_vote_number_type', 'percentage');
|
||||
$already_voted = $user_id > 0 && get_user_meta($user_id, '_jio_forum_vote_' . $id, true) !== '';
|
||||
?>
|
||||
<form class="jio-forum__vote" data-forum-form="vote" data-id="<?php echo esc_attr((string) $id); ?>"><h2><?php echo esc_html((string) get_post_meta($id, '_jio_forum_vote_title', true) ?: '参与投票'); ?></h2>
|
||||
<?php foreach ($options as $index => $option) : ?>
|
||||
<label><input type="<?php echo $vote_type === 'multiple' ? 'checkbox' : 'radio'; ?>" name="<?php echo $vote_type === 'multiple' ? 'option[]' : 'option'; ?>" value="<?php echo esc_attr((string) $index); ?>" <?php disabled($already_voted || $expired); ?> <?php if ($vote_type !== 'multiple') echo 'required'; ?>><span><?php echo esc_html($option); ?></span>
|
||||
<?php if ($vote_display !== 'hide') : ?><span class="jio-forum__vote-count"><?php echo esc_html($vote_display === 'number' ? (string) (int) ($votes[$index] ?? 0) : (string) ((int) get_post_meta($id, '_jio_forum_vote_total', true) > 0 ? round(100 * (int) ($votes[$index] ?? 0) / (int) get_post_meta($id, '_jio_forum_vote_total', true)) : 0) . '%'); ?></span><?php endif; ?></label>
|
||||
<?php endforeach; ?>
|
||||
<p class="jio-forum__message" role="status"></p><?php if ($already_voted) : ?><p>已投票</p><?php elseif ($expired) : ?><p>投票已截止</p><?php else : ?><button type="submit" class="jio-forum__primary">提交投票</button><?php endif; ?>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
<?php else : ?>
|
||||
<section class="jio-forum__locked"><h2>内容已隐藏</h2><p>请完成访问条件后查看。</p>
|
||||
<?php if ($user_id > 0) : ?>
|
||||
<?php
|
||||
$plate_rule = (string) get_post_meta($plate_id, '_jio_forum_view_limit', true);
|
||||
$post_rule = (string) get_post_meta($id, '_jio_forum_view_limit', true);
|
||||
if (in_array($plate_rule, ['points', 'pay'], true) && !\Jio\Forum\Commerce::has_plate_entitlement($user_id, $plate_id)) {
|
||||
View::action('plate-unlock', $plate_id, '解锁版块');
|
||||
} elseif (($plate_rule === 'follow' || $post_rule === 'follow') && !Access::follows($user_id, $plate_id)) {
|
||||
echo '<a href="' . esc_url(get_permalink($plate_id)) . '">关注版块</a>';
|
||||
} elseif (in_array($post_rule, ['points', 'pay'], true)) {
|
||||
View::action('unlock', $id, '解锁帖子');
|
||||
}
|
||||
?>
|
||||
<?php else : ?><a href="<?php echo esc_url(jio_get_login_url(get_permalink($id))); ?>">登录查看</a><?php endif; ?>
|
||||
</section>
|
||||
<?php endif; ?>
|
||||
<div class="jio-forum__actions">
|
||||
<?php View::action('score', $id, '赞 ' . (int) get_post_meta($id, '_jio_forum_score', true)); ?>
|
||||
<button type="button" class="jio-forum__action" data-forum-action="score" data-direction="down" data-id="<?php echo esc_attr((string) $id); ?>">踩</button>
|
||||
<?php View::action('favorite', $id, '收藏'); ?>
|
||||
<?php View::action('report', $id, '举报'); ?>
|
||||
<?php if ($can_manage) : ?><a href="<?php echo esc_url(Core::edit_url($id)); ?>">编辑</a><?php View::action('delete', $id, '删除'); endif; ?>
|
||||
<?php if ($can_moderate) : View::action('essence', $id, '精华'); View::action('pin', $id, '置顶'); View::action('audit', $id, '审核'); View::action('comments', $id, $post->comment_status === 'open' ? '关闭回复' : '开放回复'); View::action('move', $id, '移动版块'); endif; ?>
|
||||
</div>
|
||||
</article>
|
||||
<?php if ($can_view) : ?>
|
||||
<?php if ($type === 'question' && get_post_meta($id, '_jio_forum_adopted_comment', true)) : ?><p class="jio-forum__adopted">已采纳回复 #<?php echo (int) get_post_meta($id, '_jio_forum_adopted_comment', true); ?></p><?php endif; ?>
|
||||
<?php View::replies($id); ?>
|
||||
<?php endif; ?>
|
||||
</div><?php View::sidebar('post'); ?></div>
|
||||
<?php \Jio\WidgetSystem\WidgetAreas::render('forum_post_bottom_fluid'); View::close(); ?>
|
||||
\Jio\Forum\PostView::render();
|
||||
@@ -0,0 +1,63 @@
|
||||
const test = require('node:test')
|
||||
const assert = require('node:assert/strict')
|
||||
const fs = require('node:fs')
|
||||
const path = require('node:path')
|
||||
|
||||
const root = path.resolve(__dirname, '..', '..')
|
||||
const read = (file) => fs.readFileSync(path.join(root, file), 'utf8')
|
||||
|
||||
test('plate pages render the reference header card, tab bar and list', () => {
|
||||
const listing = read('src/Forum/ListingView.php')
|
||||
const cards = read('src/Forum/Cards.php')
|
||||
const view = read('src/Forum/View.php')
|
||||
assert.match(view, /public static function listing\(\): void \{\s*ListingView::plate\(\);/)
|
||||
for (const token of ['plate-tab', 'tab-nav-theme', 'affix-header-sm', 'data-forum-tab', 'data-forum-more', 'post_ajax_loader', 'content-wrap', 'content-layout', 'sidebar']) {
|
||||
assert.ok(listing.includes(token), `plate page missing ${token}`)
|
||||
}
|
||||
// Reference header: blurred cover + mask + stats + moderator avatars.
|
||||
for (const token of ['plate-header-wrap', 'forum-header blur-header', 'abs-blur-bg', 'forum-mask', 'moderator-btns', 'count-mates']) {
|
||||
assert.ok(cards.includes(token), `plate header missing ${token}`)
|
||||
}
|
||||
// Moderator-only tabs (pending / trash) appear on plate pages.
|
||||
assert.match(listing, /'pending'/)
|
||||
assert.match(listing, /'trash'/)
|
||||
// Topping posts are pulled out of the main loop on the first tab.
|
||||
assert.match(listing, /_jio_forum_pinned/)
|
||||
assert.match(listing, /post__not_in/)
|
||||
})
|
||||
|
||||
test('post detail pages use the reference article shell', () => {
|
||||
const post = read('src/Forum/PostView.php')
|
||||
const template = read('templates/pages/page-forum-post.php')
|
||||
assert.match(template, /PostView::render\(\)/)
|
||||
for (const token of ['fixed-wrap single-wrap', 'fixed-wrap-content', 'forum-single-cover', 'breadcrumb', 'id="article"', 'article zib-widget forum-article', 'article-header clearfix', 'forum-article-meta', 'wp-posts-content', 'article-tags', 'forum-article-footer', 'footer-actions', 'vote-box forum-vote', 'comments-card']) {
|
||||
assert.ok(post.includes(token), `post page missing ${token}`)
|
||||
}
|
||||
// Banners, permissions and actions stay server-side.
|
||||
assert.match(post, /Access::can_view\(/)
|
||||
assert.match(post, /Cards::title\(\$post, 'article-title'/)
|
||||
assert.match(post, /data-forum-action="score"/)
|
||||
assert.match(post, /jio_is_show_sidebar\(\)/)
|
||||
})
|
||||
|
||||
test('compose pages mirror the reference editor plus settings sidebar', () => {
|
||||
const edit = read('src/Forum/EditView.php')
|
||||
const template = read('templates/pages/page-forum-edit.php')
|
||||
assert.match(template, /EditView::render\(\)/)
|
||||
for (const token of ['editor-main-box', 'edit-posts-featured', 'edit-posts-title', 'line-form-input', 'jio_forum_content', 'bbs-posts-edit', 'but-average', 'bbs-posts-submit', 'data-forum-draft', 'drop-select', 'allow-view-drop', 'plate-drop', 'type-drop', 'forum-type-vote']) {
|
||||
assert.ok(edit.includes(token), `compose page missing ${token}`)
|
||||
}
|
||||
assert.match(edit, /class="sidebar show-sidebar"/)
|
||||
assert.match(edit, /wp_editor\(/)
|
||||
})
|
||||
|
||||
test('compose page saves draft state and the cover through the REST contract', () => {
|
||||
const rest = read('src/Forum/RestPosts.php')
|
||||
const runtime = read('assets/js/forum-runtime.js')
|
||||
assert.match(rest, /\$requested === 'draft'/)
|
||||
assert.match(rest, /featured_image_id/)
|
||||
assert.match(rest, /set_post_thumbnail\(/)
|
||||
for (const token of ['data-forum-type-set', 'data-forum-view', 'data-forum-plate', 'data-forum-status', 'data-forum-topics', 'data-forum-tags', 'data-forum-cover-file']) {
|
||||
assert.ok(runtime.includes(token), `compose runtime missing ${token}`)
|
||||
}
|
||||
})
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"generated_at": "2026-11-11T07:46:58+00:00",
|
||||
"generated_at": "2026-11-11T08:16:48+00:00",
|
||||
"errors": [],
|
||||
"warnings": [
|
||||
"customPage tools-update section 'tools/update' not in PHP schema (ok if pure operational page)",
|
||||
|
||||
Reference in new issue
Block a user