From ec5dfcd82f147f6bbe99179fb2c6dd81e40e42ab Mon Sep 17 00:00:00 2001 From: unknown Date: Wed, 11 Nov 2026 18:06:33 +0800 Subject: [PATCH] fix(forum): align forum pages with the reference layout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - cards: restore the shared card shell, add author follow/message actions, score group, last-reply meta and reference plate header actions - home: desktop left tab rail, widget-only sidebar, reference load-more markup - plate: 1-based tabs, reference defaults (全部/推荐/最新回复/热门/精华) - post: fixed action rail, score footer, author card, h1 article title - quick publish: plate selector moved into the submit row like the reference --- assets/css/routes/forum.css | 150 +++++++++++++++++++--------- assets/js/forum-runtime.js | 25 ++++- src/Admin/Schema/SchemaForum.php | 10 +- src/Forum/Access.php | 4 + src/Forum/Cards.php | 103 +++++++++++++++---- src/Forum/Core.php | 21 +++- src/Forum/EditView.php | 22 ++-- src/Forum/HomeContent.php | 4 +- src/Forum/HomeTabs.php | 8 +- src/Forum/HomeView.php | 36 ++----- src/Forum/ListingView.php | 57 +++++------ src/Forum/PostView.php | 87 ++++++++++++++-- src/Forum/QuickPost.php | 31 +++--- tests/forum/pages-contract.test.cjs | 2 +- tools/admin-contract-report.json | 2 +- 15 files changed, 396 insertions(+), 166 deletions(-) diff --git a/assets/css/routes/forum.css b/assets/css/routes/forum.css index 3b9a9c12..a1f711d0 100644 --- a/assets/css/routes/forum.css +++ b/assets/css/routes/forum.css @@ -24,44 +24,44 @@ #forum .jio-forum__tabs a{padding:14px 0;color:var(--forum-muted);border-bottom:2px solid transparent} #forum .jio-forum__tabs .is-active{color:var(--forum-accent);border-color:var(--forum-accent);font-weight:600} -/* ── 论坛首页:栏目 TAB ─────────────────────────────────────────── */ +/* ── 论坛首页:栏目 TAB(桌面端为左侧竖排导航,移动端横排滑动) ───────── */ #forum.forum-home{padding-bottom:40px} -.forum-home .forum-home__main{min-width:0} +.forum-home .content-layout{min-width:0} .forum-home-tab .fixed-wrap-nav{margin-bottom:10px} .forum-home-tab .tab-nav-theme{border-bottom:none;padding:0;margin:0} -.forum-home-tab .tab-nav-theme li{padding:0 2px} -.forum-home-tab .tab-nav-theme li > a{display:block;padding:10px 12px;font-size:16px;color:var(--muted-2-color)} -.forum-home-tab .tab-nav-theme li.active > a{color:var(--focus-color);font-weight:600} +.forum-home-tab .tab-nav-theme li{padding:0} +.forum-home-tab .tab-nav-theme li > a{display:block;padding:6px;margin:1px 0;font-size:16px;color:var(--muted-2-color);overflow:hidden} +.forum-home-tab .tab-nav-theme li > a:hover{color:var(--main-color)} +.forum-home-tab .tab-nav-theme li.active > a{color:var(--focus-color);font-weight:500;background:var(--focus-color-opacity05);border-radius:var(--mini-radius)} +.forum-home-tab .tab-nav-theme li::before{display:none} .forum-home-tab .tab-content > .tab-pane{display:none} .forum-home-tab .tab-content > .tab-pane.active{display:block} .forum-main-tab-content{min-height:120px} -.forum-more{padding:20px 0;text-align:center} -.forum-more.is-loading{display:none} .forum-tab-pane > .ajax-item:last-of-type{margin-bottom:0} -/* 首页栏目是横向 TAB,覆盖通用 .fixed-wrap 的竖排侧栏导航形态 */ -@media (min-width:768px){ - .forum-home-tab{--fixed-width:auto} - .forum-home-tab > .fixed-wrap-nav{position:static;width:auto} - .forum-home-tab > .fixed-wrap-nav + .fixed-wrap-content{margin-left:0} - .forum-home-tab .fixed-wrap-nav.affix{position:sticky;left:auto;right:auto;width:auto;z-index:11;background:var(--main-bg-color);box-shadow:0 0 10px var(--main-shadow)} - .forum-home-tab .tab-nav-theme.mini-scrollbar, - .forum-home-tab .tab-nav-theme.swiper-scroll .swiper-wrapper{flex-direction:row;overflow-x:auto;overflow-y:hidden;text-align:left;white-space:nowrap;border-bottom:none;margin:0;padding:0} - .forum-home-tab .tab-nav-theme.mini-scrollbar > li, - .forum-home-tab .tab-nav-theme.swiper-scroll .swiper-wrapper > li{display:inline-block;padding:0;margin:0} +@media (max-width:767px){ + .forum-home-tab .fixed-wrap-nav:not(.affix):not(.affix-bottom){margin-bottom:10px;padding:0 5px;background:transparent;box-shadow:none} + .forum-home-tab .tab-nav-theme{display:flex;flex-direction:row;overflow-x:auto;overflow-y:hidden;text-align:left;white-space:nowrap;border-bottom:none;margin:0;padding:0} + .forum-home-tab .tab-nav-theme > li{display:inline-block;padding:0;margin:0;flex:0 0 auto} + .forum-home-tab .tab-nav-theme > li > a{padding:8px 10px;margin:0;font-size:15px;background:none!important} } .quick-posts-box{border-radius:var(--main-radius);padding:15px} .quick-posts-box .title-theme{margin-bottom:10px} .quick-input-group{padding:15px;border:1px solid var(--main-border-color);border-radius:var(--main-radius)} .quick-posts-box .edit-posts-title{position:relative} .quick-posts-box .edit-posts-title .line-form-line{display:none} -.quick-posts-box .edit-posts-title textarea{width:100%;color:var(--main-color);padding:0;margin-bottom:10px;font-weight:500;font-size:18px;border:0;border-bottom:1px solid var(--main-border-color);background:transparent;resize:none;outline:0} -.quick-posts-box .edit-posts-content textarea{outline:0!important;border:none;display:block;width:100%;padding:0;background:transparent;color:var(--main-color);resize:vertical;min-height:88px} +.quick-posts-box .edit-posts-title textarea{width:100%;color:var(--main-color);opacity:1;padding:0;margin-bottom:10px;font-weight:500;font-size:18px;border:0;border-bottom:1px solid var(--main-border-color);background:transparent;resize:none;outline:0} +.quick-posts-box .edit-posts-content textarea{outline:0!important;border:none;display:block;width:100%;padding:0;margin-bottom:10px;background:var(--main-bg-color);opacity:.9;color:var(--main-color);resize:vertical;min-height:88px} .quick-posts-box .preview .add,.quick-posts-box .preview .preview-item{width:90px;height:90px;border:1px dashed var(--main-border-color);background:none;margin:2px;border-radius:var(--main-radius);position:relative} .quick-posts-box .preview .add:before{content:"+";position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:28px;color:var(--muted-3-color)} .quick-posts-box .drop-select .dropdown-menu{min-width:320px} -.quick-more-btns{color:var(--muted-color);position:relative;flex-wrap:wrap;gap:6px} -.quick-more-btns .drop-btn{padding:6px 4px;cursor:pointer} +.quick-posts-box .dropdown-menu.fluid::before{left:20px} +.quick-posts-box .but-plate{font-size:14px;padding:5px 9px 6px 5px} +.quick-more-btns{color:var(--muted-color);position:relative;flex-wrap:wrap} +.quick-more-btns .drop-btn{padding:6px 4px;cursor:pointer;white-space:nowrap} .quick-more-btns .drop-select{position:relative} +.quick-selected-tags > *{display:inline-block} +.quick-selected-tags > *:empty{display:none} +.quick-selected-tags [name="topics"]{color:var(--focus-color);margin-top:6px;font-size:13px} .quick-posts-submit .but{padding:6px 5px;min-width:85px} .quick-posts-submit .quick-captcha{margin-bottom:8px} .hide-type-drop .select-drop-box .badg.active{--this-bg:var(--focus-color);--this-color:#fff} @@ -75,6 +75,15 @@ .quick-posts-form .jio-forum__message:empty{display:none} .forum-pending-plate{background:var(--main-bg-color);box-shadow:0 0 10px var(--main-shadow);border-radius:var(--main-radius);padding:12px 15px;margin-bottom:10px} +/* ── 论坛首页 / 版块页的加载更多(与主题 AJAX 翻页按钮同构) ───────── */ +.forum-tab-pane .theme-pagination,.plate-tab .theme-pagination{margin:0} +.forum-tab-pane .theme-pagination.ajax-pag,.plate-tab .theme-pagination.ajax-pag{padding:15px 5px 10px} +.forum-tab-pane .theme-pagination .ajax-next a,.plate-tab .theme-pagination .ajax-next a{color:var(--muted-color);opacity:.6;border-radius:30px;padding:5px 15px;display:inline-block} +.forum-tab-pane .theme-pagination .ajax-next a:hover,.plate-tab .theme-pagination .ajax-next a:hover{background:var(--main-border-color);opacity:1} +.forum-tab-pane .theme-pagination .ajax-next .fa,.plate-tab .theme-pagination .ajax-next .fa{margin-right:15px;transition:.3s} +.forum-tab-pane .theme-pagination .ajax-next a:hover .fa,.plate-tab .theme-pagination .ajax-next a:hover .fa{margin-right:8px} +.forum-tab-pane.is-loading .theme-pagination,.plate-tab .forum-tab-pane.is-loading .theme-pagination{display:none} + /* ── 版块面板 / 版块卡片 ────────────────────────────────────────── */ .panel.panel-plate{border-color:transparent;margin-bottom:10px;background:var(--main-bg-color);border-radius:var(--main-radius);box-shadow:0 0 10px var(--main-shadow);padding:0 15px} .panel.panel-plate:first-of-type .panel-title{padding-top:10px} @@ -94,9 +103,10 @@ .scroll-plate .plate-card.mini{padding:15px 5px;width:85px} .scroll-plate .plate-card .plate-thumb{width:50px!important;height:50px!important} .follow-plate .plate-card{width:90px} -.plate-item{display:inline-flex;align-items:flex-start} +.plate-item{display:inline-flex} .plate-lists{display:flex;align-content:flex-start;flex-flow:row wrap;margin:-3px} .plate-lists .plate-item{margin:3px;background:var(--main-bg-color);box-shadow:0 0 10px var(--main-shadow);border-color:transparent;border-radius:var(--main-radius);padding:15px 20px;width:calc(50% - 6px);position:relative;box-sizing:border-box} +.sidebar .plate-lists .plate-item{width:calc(100% - 6px)} .plate-item .plate-thumb{margin-right:15px;flex-shrink:0;display:inline-block;width:60px;height:60px;position:relative} .plate-item .plate-thumb > img,.plate-thumb > img{border-radius:100px;width:100%;height:100%} .plate-item .forum-thumbnail{transition:all .2s;object-fit:cover;width:100%;height:100%} @@ -165,10 +175,10 @@ .forum-posts.detail .user-info ~ *{margin-left:70px} .forum-posts.detail .user-info .display-name{font-size:16px} .forum-posts.detail .action-meta .btn-score:hover,.forum-posts.detail .action-meta a.item:hover{background:var(--main-border-color)} + .forum-header .buts .but{min-width:87px} + .blur-header .count-mates{--main-border-color:rgba(255,255,255,.08)} } @media(max-width:800px){ - #forum .jio-forum__layout{grid-template-columns:1fr} - #forum .jio-forum__aside{border-left:0;padding-left:0} .forum-home .sidebar{width:100%;margin-left:0;margin-top:20px} .forum-home .content-layout{margin-right:0!important;margin-left:0!important} } @@ -301,35 +311,53 @@ body.dark-theme .panel.panel-plate,body.dark-theme .plate-lists .plate-item,body 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} +.forum-listing .content-layout{min-width:0} +.term-main{max-width:790px;margin:0 auto 20px} .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-nav-theme{margin:0} +.plate-tab .tab-nav-theme li a{font-size:16px;color:var(--muted-2-color);display:block;padding:0 5px} +.plate-tab .tab-nav-theme li.active a{font-weight:700;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{overflow:hidden;color:#ddd} +.forum-header.blur-header .header-content{background:none;color:#fff;padding:70px 50px 50px} +.forum-header.blur-header .forum-title,.forum-header.blur-header .forum-title a,.forum-header.blur-header 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.blur-header .plate-thumb{width:115px;height:115px;flex-shrink:0} +.forum-header.blur-header .plate-thumb > img{border-radius:4px} +.forum-header .more-btns{position:absolute;right:5px;top:3px;z-index:1;justify-content:flex-end} +.forum-header .more-btns .item{padding:5px 7px;opacity:.8;font-size:1.2em;color:#fff;border:0;background:none;cursor:pointer} +.forum-header .more-btns .item text{display:none} .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-header .moderator-btns .avatar-mini{--this-size:22px;margin-right:4px;display:inline-block} +.forum-header .moderator-btns .avatar-mini img{border-radius:100px;width:100%;height:100%} +.forum-header .moderator-btns .but{font-size:12px;padding:3px 10px;line-height:21px;min-width:90px} +.forum-header .count-mates{margin-top:10px} +.forum-header .count-mates item{display:inline-block} +.forum-header .count-mates > item + item{border-left:1px solid rgba(255,255,255,.08);padding-left:20px;margin-left:20px} +.blur-header .c-white{--this-bg:rgba(255,255,255,.12);--this-color:#fff} +.forum-header .more-btns .btn-follow{color:#fff;border:0;background:none;cursor:pointer;opacity:.85;padding:5px 7px;font-size:1.2em} +.forum-header .more-btns .btn-follow.active{color:#ff7800;opacity:1} /* ── 帖子详情页 ───────────────────────────────────────────────── */ .forum-post .single-wrap{width:100%} +.single-fixed-btns{display:none} +@media (min-width:768px){ + .forum-post .fixed-wrap-nav.single-fixed-btns{display:block} + .single-fixed-btns .score-btns{display:block;text-align:center} + .single-fixed-btns .score-btns text{display:block;font-size:12px;color:var(--muted-2-color);min-width:30px;margin:5px} + .single-fixed-btns .btn-score{font-size:24px;color:var(--muted-color);display:inline-flex;align-items:center} + .single-fixed-btns .btn-score.extra.active{color:#40a1ff} + .single-fixed-btns .btn-score.deduct.active{color:#ff556d} +} .forum-single-cover{border-radius:var(--main-radius);overflow:hidden} -.forum-article{padding:20px 26px} +.forum-article{padding:0 30px;overflow:hidden} .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)} @@ -340,18 +368,32 @@ body.dark-theme .forum-header,body.dark-theme .forum-header .header-content{back .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} +.forum-article .score-box.single-footer{margin:10px 0 20px} +.forum-article .score-box .score-btns{margin-bottom:10px;border-radius:100px;padding:8px 24px;box-shadow:0 2px 10px 4px var(--main-shadow);display:inline-flex;align-items:center} +.forum-article .score-box .btn-score{font-size:24px;color:var(--muted-color);padding:2px} +.forum-article .score-box .btn-score.extra.active{color:#40a1ff} +.forum-article .score-box .btn-score.deduct.active{color:#ff556d} +.forum-article .score-box .score-btns text{font-size:12px;color:var(--muted-2-color);min-width:30px;margin:5px} +.forum-single-author{background-color:var(--muted-bg-2-color);box-shadow:none} +.forum-single-author .card-content{display:flex;gap:40px;padding:15px 40px} +.forum-single-author .user-content{display:flex;align-items:center;gap:20px} +.forum-single-author .count-info{display:flex;flex-direction:column;gap:6px} +.forum-single-author .user-info .border-bottom{padding-bottom:10px;margin-bottom:10px} +.forum-single-author .user-desc{font-size:13px} +.forum-single-author .more-posts-mini .item{padding:3px 0} +.forum-single-author .more-posts-mini a{display:block;color:var(--muted-2-color);overflow:hidden;text-overflow:ellipsis;white-space:nowrap} .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{display:flex;align-items:center;padding:0 4px;margin:0 4px;color:var(--muted-color);border:0;background:none;cursor:pointer} +.footer-actions .item:hover{color:var(--focus-color)} +.footer-actions .item > i,.footer-actions .item > .icon{font-size:20px;margin-right:4px} .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{padding:15px 30px} .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{position:relative;overflow:hidden;padding:5px 3.5em 5px 15px;margin-bottom:6px;border-radius:50px;background:var(--main-bg-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-progress{position:absolute;left:0;top:0;bottom:0;background:rgba(130,130,130,.15);transition:width 1s,background .2s;border-radius:50px;width:0;z-index:-1} +.vote-item .vote-count{position:absolute;right:14px;font-size:.9em;margin-top:.1em;opacity:.8} .vote-box.forum-vote button{display:block;margin:16px auto 0} /* ── 发帖 / 编辑页 ────────────────────────────────────────────── */ @@ -384,3 +426,21 @@ body.dark-theme .forum-header,body.dark-theme .forum-header .header-content{back .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)} + +/* ── 详情页侧栏版块信息卡(参考 .plate-info-sidebar) ─────────────── */ +.plate-info-sidebar .forum-header{box-shadow:0 0 10px var(--main-shadow);border-radius:var(--main-radius)} +.plate-info-sidebar .header-content{padding:55px 20px 25px;background:var(--main-bg-color);border-radius:var(--main-radius)} +.plate-info-sidebar .plate-thumb{width:100px;height:100px;margin:0 auto 10px} +.plate-info-sidebar .plate-thumb > img{border-radius:4px} +.plate-info-sidebar .forum-title{font-size:16px;text-align:center} +.plate-info-sidebar .count-mates{text-align:center;color:var(--muted-2-color)} +.plate-info-sidebar .count-mates > item + item{border-left:1px solid var(--main-border-color);padding-left:12px;margin-left:12px} +.plate-info-sidebar .follow-info{background:var(--main-border-color);box-shadow:none;color:var(--focus-color);padding:6px 12px;margin-top:38px;opacity:.9} +@media (max-width:640px){ + .forum-single-author .card-content{flex-direction:column;gap:20px;padding:5px 12px} + .forum-single-author .user-info .border-bottom{padding-bottom:10px;margin-bottom:10px} + .forum-article{padding:0 15px} + .forum-article .article-title{font-size:20px} + .forum-header.blur-header .header-content{padding:60px 15px 30px} + .forum-header.blur-header .plate-thumb{width:105px;height:105px;order:2;margin:0 0 0 20px} +} diff --git a/assets/js/forum-runtime.js b/assets/js/forum-runtime.js index dbf5885b..517e5a21 100644 --- a/assets/js/forum-runtime.js +++ b/assets/js/forum-runtime.js @@ -359,6 +359,24 @@ const label = button.querySelector('text') if (label) label.textContent = following ? '已关注' : '关注' else button.textContent = result.data?.label || button.textContent + } else if (action === 'favorite') { + const active = result.data?.active === true + button.classList.toggle('active', active) + const label = button.querySelector('text') + if (label) label.textContent = active ? '已收藏' : '收藏' + } else if (action === 'score') { + // Card score group: flip the active arrow and refresh the shared counter. + const group = button.closest('.score-btns') + if (group) { + const isUp = button.dataset.direction !== 'down' + const userScore = Number(result.data?.user_score || 0) + group.querySelectorAll('.btn-score').forEach((item) => { + const own = (item.dataset.direction !== 'down') === isUp + item.classList.toggle('active', own && userScore !== 0) + }) + const counter = group.querySelector('text') + if (counter) counter.textContent = Number(result.data?.score || 0) > 0 ? String(result.data.score) : '评分' + } } else if (button.querySelector('text')) { button.querySelector('text').textContent = result.data?.label || result.message || button.querySelector('text').textContent } else { @@ -403,7 +421,7 @@ const loadMore = async (link, pane) => { if (pane.dataset.loading === '1') return pane.dataset.loading = '1' - const more = link.closest('.forum-more') + const more = link.closest('.theme-pagination') || link.closest('.ajax-pag') const loader = [...pane.children].find((item) => item.classList.contains('post_ajax_loader')) more?.classList.add('is-loading') loader?.classList.add('is-loading') @@ -414,7 +432,7 @@ const source = doc.getElementById(pane.id) if (!source) throw new Error('内容不存在') const anchor = more || null - const incoming = [...source.children].filter((item) => !item.classList.contains('forum-tab-pane') && !item.classList.contains('forum-more') && !item.classList.contains('post_ajax_loader')) + const incoming = [...source.children].filter((item) => !item.classList.contains('forum-tab-pane') && !item.classList.contains('theme-pagination') && !item.classList.contains('post_ajax_loader')) incoming.forEach((item) => pane.insertBefore(item, anchor)) const next = source.querySelector('[data-forum-more]') if (next && anchor) anchor.querySelector('[data-forum-more]')?.setAttribute('href', next.getAttribute('href')) @@ -449,7 +467,10 @@ } const more = event.target.closest('[data-forum-more]') if (more) { + // The wrapper also carries .ajax-next for the theme's pagination styles; + // stop the generic handler from firing a second request. event.preventDefault() + event.stopPropagation() const pane = more.closest('.forum-tab-pane') if (pane) loadMore(more, pane) } diff --git a/src/Admin/Schema/SchemaForum.php b/src/Admin/Schema/SchemaForum.php index c12b8a00..c352487f 100644 --- a/src/Admin/Schema/SchemaForum.php +++ b/src/Admin/Schema/SchemaForum.php @@ -53,16 +53,17 @@ 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_active', '版块页默认栏目', 'number', 2, ['min' => 1, 'max' => 20, 'description' => '打开版块页面时默认显示第几个栏目']), self::field('jio_forum_plate_tab_items', '版块页栏目', 'repeater', self::default_plate_tabs(), [ 'addText' => '添加栏目', - 'description' => '版块页顶部的栏目条;留空则使用「最新发布 / 最近回复 / 热门 / 精华」默认栏目', + 'description' => '版块页顶部的栏目条;留空则使用「全部 / 推荐 / 最新回复 / 热门 / 精华」默认栏目', 'fields' => [ self::field('title', '栏目标题', 'input', ''), self::field('orderby', '排序方式', 'select', 'date', ['options' => self::options([ 'date' => '发布时间', 'modified' => '修改时间', 'last_reply' => '最新回复', + 'popular' => '推荐指数', 'views' => '阅读量', 'score' => '评分', 'comment_count' => '回复数量', @@ -116,8 +117,9 @@ 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' => 'modified', 'filter' => '', 'style' => 'mini'], + ['title' => '推荐', 'orderby' => 'popular', 'filter' => '', 'style' => 'detail'], + ['title' => '最新回复', 'orderby' => 'last_reply', 'filter' => '', 'style' => 'detail'], ['title' => '热门', 'orderby' => 'views', 'filter' => '', 'style' => 'detail'], ['title' => '精华', 'orderby' => 'modified', 'filter' => 'essence', 'style' => 'detail'], ]; diff --git a/src/Forum/Access.php b/src/Forum/Access.php index 0639a838..d90ad5de 100644 --- a/src/Forum/Access.php +++ b/src/Forum/Access.php @@ -66,6 +66,10 @@ final class Access { return $user_id > 0 && (bool) get_user_meta($user_id, '_jio_forum_follow_' . $plate_id, true); } + public static function favorites(int $user_id, int $post_id): bool { + return $user_id > 0 && (bool) get_user_meta($user_id, '_jio_forum_favorite_' . $post_id, true); + } + public static function can_view(int $user_id, int $post_id): bool { $post = get_post($post_id); if (!$post instanceof \WP_Post || $post->post_type !== Core::POST) { diff --git a/src/Forum/Cards.php b/src/Forum/Cards.php index 30a8b7f2..544ecb9d 100644 --- a/src/Forum/Cards.php +++ b/src/Forum/Cards.php @@ -7,8 +7,10 @@ defined('ABSPATH') || exit; final class Cards { /** @var array */ private static array $hot = []; + /** @var array */ + private static array $last_reply = []; - /** Prime authors, plates and hot replies for a page of posts in batch queries. */ + /** Prime authors, plates, hot replies and last replies for a page of posts in batch queries. */ public static function prime(array $posts): void { $ids = $authors = $plates = []; foreach ($posts as $post) { @@ -26,6 +28,20 @@ final class Cards { $post_id = (int) $comment->comment_post_ID; if (isset($wanted[$post_id]) && !isset(self::$hot[$post_id])) self::$hot[$post_id] = $comment; } + self::prime_last_replies($ids); + } + + /** @param int[] $ids */ + private static function prime_last_replies(array $ids): void { + $missing = array_values(array_diff(array_unique(array_map('absint', $ids)), array_keys(self::$last_reply))); + if (!$missing) return; + global $wpdb; + $placeholders = implode(',', array_fill(0, count($missing), '%d')); + $sql = "SELECT comment_post_ID AS post_id, MAX(comment_date) AS last_date FROM {$wpdb->comments} WHERE comment_approved = '1' AND comment_post_ID IN ($placeholders) GROUP BY comment_post_ID"; + $rows = $wpdb->get_results($wpdb->prepare($sql, $missing)); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + foreach ((array) $rows as $row) { + self::$last_reply[(int) $row->post_id] = (string) $row->last_date; + } } public static function post_detail(\WP_Post $post, string $class = 'alone ajax-item', bool $show_plate = true, bool $show_topping = false): string { @@ -51,12 +67,21 @@ final class Cards { $html .= '
' . self::avatar($author, 'avatar-img forum-avatar') . '' . $name . '
'; $html .= ''; return $html . ''; } - public static function title(\WP_Post $post, string $class, string $link_class = '', bool $show_topping = false): string { + /** Reference mini-card meta: "最后回复" when replies exist, otherwise the publish time. */ + private static function reply_time(\WP_Post $post): string { + $last = self::$last_reply[$post->ID] ?? ''; + if ($last !== '') { + return '' . esc_html(self::ago($last) . '回复') . ''; + } + return '' . esc_html(self::ago($post->post_date) . '发布') . ''; + } + + public static function title(\WP_Post $post, string $class, string $link_class = '', bool $show_topping = false, string $tag = 'h2'): string { $id = $post->ID; $badges = ''; if ($show_topping && get_post_meta($id, '_jio_forum_pinned', true)) $badges .= '置顶'; @@ -70,7 +95,8 @@ final class Cards { } $url = $post->post_status === 'publish' ? get_permalink($post) : Core::edit_url($id); $link_class = $link_class !== '' ? ' class="' . esc_attr($link_class) . '"' : ''; - return '

' . $badges . esc_html($post->post_title) . '

'; + $tag = in_array($tag, ['h1', 'h2'], true) ? $tag : 'h2'; + return '<' . $tag . ' class="' . esc_attr($class) . '">' . $badges . esc_html($post->post_title) . ''; } /** Plate page header: blurred cover, title, stats and moderators (reference layout). */ @@ -94,6 +120,7 @@ final class Cards { $excerpt = wp_trim_words(wp_strip_all_tags($plate->post_excerpt ?: $plate->post_content), 40); $html = '
'; $html .= '
' . $thumb . '
'; + $html .= self::plate_more_btns($plate); if (get_post_meta($id, '_jio_forum_hot', true)) { $html .= '热门'; } @@ -107,18 +134,32 @@ final class Cards { return $html . '
'; } + /** Header corner actions: follow, search and share (reference `.more-btns`). */ + private static function plate_more_btns(\WP_Post $plate): string { + $html = '
' . self::follow_button($plate->ID, ''); + if (jio_get_setting('jio_forum_search_enabled', true)) { + $html .= ''; + } + if (function_exists('jio_get_post_share_btn')) { + $html .= jio_get_post_share_btn($plate, 'item btn-share', true, true); + } + return $html . '
'; + } + /** 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 .= '' . get_avatar($user_id, 40) . '' . esc_html($user->display_name) . ''; + $btns .= '' . get_avatar($user_id, 40) . '' . esc_html($user->display_name) . ''; + } + if (is_user_logged_in() && Access::can_publish(get_current_user_id(), $plate->ID)) { + $btns .= '发布'; } $html = '
' . $btns; if (is_user_logged_in() && !Access::is_moderator(get_current_user_id(), $plate->ID) && jio_get_setting('jio_forum_moderator_apply', true)) { @@ -127,7 +168,8 @@ final class Cards { return $html . '
'; } - public static function plate_item(\WP_Post $plate): string { $url = get_permalink($plate); + public static function plate_item(\WP_Post $plate): string { + $url = get_permalink($plate); $counts = PlateStats::get($plate->ID); $today = $counts['today'] > 0 ? '' . esc_html(sprintf('今日:%s', jio_cut_count($counts['today']))) . '' : ''; $excerpt = wp_trim_words(wp_strip_all_tags($plate->post_excerpt ?: $plate->post_content), 30); @@ -165,10 +207,10 @@ final class Cards { public static function follow_button(int $plate_id, string $class): string { if (!is_user_logged_in()) { - return '关注'; + return ''; } $followed = Access::follows(get_current_user_id(), $plate_id); - return ''; + return ''; } public static function plate_button(int $plate_id): string { @@ -185,15 +227,29 @@ final class Cards { } private static function author_link(int $user_id): string { - return '' . esc_html((string) get_the_author_meta('display_name', $user_id)) . ''; + $url = function_exists('jio_get_n') ? jio_get_n($user_id) : get_author_posts_url($user_id); + return '' . esc_html((string) get_the_author_meta('display_name', $user_id)) . ''; } + /** Reference author row: avatar, name, publish time + views, follow and message actions. */ private static function author_box(\WP_Post $post): string { $author = (int) $post->post_author; $name = function_exists('jio_get_user_name') ? jio_get_user_name(['id' => $author, 'class' => 'flex ac flex1']) : self::author_link($author); $views = jio_cut_count((int) get_post_meta($post->ID, '_jio_forum_views', true)); - $desc = '' . esc_html(self::ago($post->post_date)) . '' . esc_html(sprintf('%s次阅读', $views)) . ''; - return ''; + $desc = '' . esc_html(self::ago($post->post_date) . '发布') . '' . esc_html(sprintf('%s次阅读', $views)) . ''; + $actions = ''; + if (function_exists('jio_get_user_follow')) { + $actions .= jio_get_user_follow('px12-sm ml10 follow but c-red', $author); + } + if (function_exists('jio_get_messages_page_url')) { + $messages = jio_get_messages_page_url(); + if ($messages !== '' && get_current_user_id() !== $author) { + $actions .= '私信'; + } + } + $html = ''; } private static function status_badge(\WP_Post $post): string { @@ -205,6 +261,7 @@ final class Cards { if (!has_post_thumbnail($post)) return ''; return '
' . get_the_post_thumbnail($post, 'large', ['class' => 'fit-cover']) . '
'; } + private static function content(\WP_Post $post, bool $can_view): string { if (!$can_view) { return '
此处内容已隐藏
'; @@ -237,23 +294,33 @@ final class Cards { return $html . ''; } + /** Reference action row: score up/down group, reply count and share. */ private static function action_meta(\WP_Post $post): string { $id = $post->ID; - $score = (int) get_post_meta($id, '_jio_forum_score', true); - $reply = (int) $post->comment_count; $html = '
'; if (jio_get_setting('jio_forum_score_enabled', true)) { - $html .= is_user_logged_in() - ? '' - : '' . esc_html($score ? jio_cut_count($score) : '加分') . ''; + $user_id = get_current_user_id(); + $user_score = $user_id > 0 ? (int) get_user_meta($user_id, '_jio_forum_score_' . $id, true) : 0; + $score = (int) get_post_meta($id, '_jio_forum_score', true); + $html .= '
' . self::score_button($post, 'up', 'extra', $user_score > 0) . '' . esc_html($score ? jio_cut_count($score) : '评分') . '' . self::score_button($post, 'down', 'deduct', $user_score < 0) . '
'; } + $reply = (int) $post->comment_count; $html .= '' . esc_html($reply ? jio_cut_count($reply) : '回复') . ''; if ($post->post_status === 'publish' && function_exists('jio_get_post_share_btn')) { - $html .= jio_get_post_share_btn($post, 'item btn-share', true, false); + $html .= jio_get_post_share_btn($post, 'item btn-share', true, true); } return $html . '
'; } + private static function score_button(\WP_Post $post, string $direction, string $kind, bool $active): string { + $icon = ''; + $class = 'btn-score ' . $kind . ($active ? ' active' : ''); + if (!is_user_logged_in()) { + return ''; + } + return ''; + } + private static function icon_metas(\WP_Post $post): string { $id = $post->ID; return '' . esc_html(jio_cut_count((int) get_post_meta($id, '_jio_forum_views', true))) . '' diff --git a/src/Forum/Core.php b/src/Forum/Core.php index dc02d5f8..31c19fa3 100644 --- a/src/Forum/Core.php +++ b/src/Forum/Core.php @@ -30,13 +30,32 @@ final class Core { add_action('untrashed_post', [self::class, 'restore_trashed_plate']); add_filter('wp_untrash_post_status', [self::class, 'restore_status'], 10, 2); add_filter('jio_is_show_sidebar', [self::class, 'show_home_sidebar']); + add_filter('jio_is_show_sidebar', [self::class, 'show_forum_sidebar']); } /** The forum home renders its own sidebar column, so keep the layout slot open. */ public static function show_home_sidebar(bool $show): bool { if ($show || !self::enabled() || !is_page_template('templates/pages/page-forums.php')) return $show; + return self::sidebar_has_widgets('forum_home'); + } + + /** Forum routes own their widget areas; a configured forum sidebar must open the column. */ + public static function show_forum_sidebar(bool $show): bool { + if ($show || !self::enabled()) return $show; + $context = ''; + if (is_singular(self::POST)) $context = 'forum_post'; + elseif (is_singular(self::PLATE)) $context = 'forum_plate'; + elseif (is_tax([self::CATEGORY, self::TOPIC, self::TAG])) $context = 'forum_term'; + return $context !== '' && self::sidebar_has_widgets($context); + } + + private static function sidebar_has_widgets(string $context): bool { $cfg = function_exists('jio_get_sidebar_config_safe') ? jio_get_sidebar_config_safe() : []; - return !empty($cfg['enabled']); + if (empty($cfg['enabled'])) return false; + foreach ([$context . '_sidebar', 'all_sidebar_top', 'all_sidebar_bottom'] as $area) { + if (is_active_sidebar($area)) return true; + } + return false; } public static function enabled(): bool { diff --git a/src/Forum/EditView.php b/src/Forum/EditView.php index b4979d4b..0eb10ab6 100644 --- a/src/Forum/EditView.php +++ b/src/Forum/EditView.php @@ -11,7 +11,7 @@ final class EditView { $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 '
' . jio_get_null('抱歉!您暂无编辑此内容的权限', 60, 'null-post.svg') . '
'; + echo '
' . jio_get_null('抱歉!您暂无编辑此内容的权限', 60, 'null-post.svg') . '
'; get_footer(); return; } @@ -25,7 +25,7 @@ final class EditView { wp_enqueue_editor(); echo '
'; - echo '
'; + echo '
'; if ($user_id <= 0) { echo jio_get_null('请先登录后再发布', 40, 'null-user.svg') . '

登录

'; echo '
'; @@ -73,7 +73,7 @@ final class EditView { echo self::vote_box($vote_options, $post); echo self::hide_box($post); if (!$post && function_exists('jio_render_captcha') && function_exists('jio_is_captcha_enabled_for') && jio_is_captcha_enabled_for('forum_post')) { - echo '
'; + echo '
'; jio_render_captcha('forum_post'); echo '
'; } @@ -90,7 +90,7 @@ final class EditView { private static function submit_box(?\WP_Post $post): string { $last = $post ? get_the_modified_time('Y-m-d H:i:s', $post->ID) : ''; - $html = '
'; + $html = '
'; if (!$post || !in_array($post->post_status, ['publish', 'pending'], true)) { $html .= ''; } @@ -109,7 +109,7 @@ final class EditView { foreach (['pending' => '待审核', 'draft' => '草稿', 'publish' => '发布'] as $key => $label) { $list .= '' . $label . ''; } - return '
设置状态
' . $list . '
'; + return '
设置状态
' . $list . '
'; } private static function allow_view_box(string $current): string { @@ -118,7 +118,7 @@ final class EditView { foreach ($labels as $key => $label) { $options .= '
' . esc_html($label) . '
'; } - return ''; + return ''; } private static function plate_box(int $current): string { @@ -128,7 +128,7 @@ final class EditView { $options .= '
' . esc_html($plate->post_title) . '
'; } $name = $current > 0 ? get_the_title($current) : '请选择'; - return ''; + return ''; } /** @param string[] $selected */ @@ -140,7 +140,7 @@ final class EditView { $active = in_array($term->name, $selected, true); $options .= ''; } - return ''; + return ''; } private static function type_box(string $current): string { @@ -153,7 +153,7 @@ final class EditView { foreach ($options as $key => $label) { $list .= '' . esc_html($label) . ''; } - return '
发布类型
' . $list . '
'; + return '
发布类型
' . $list . '
'; } /** Hidden-content composer: the block becomes a site visibility shortcode on save. */ @@ -162,7 +162,7 @@ final class EditView { foreach (['logged' => '登录可见', 'reply' => '评论可见', 'vip1' => '会员可见', 'payshow' => '付费可见'] as $key => $label) { $types .= '' . esc_html($label) . ''; } - return '
'; get_footer(); } + /** Sidebar is widget-only, exactly like the reference home column. */ private static function sidebar(): void { - $plate_name = (string) jio_get_setting('jio_forum_plate_name', '版块') ?: '版块'; - echo '