feat(forum): rebuild forum home page

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
kate522andClaude Fable 5 committed 2026-11-11 14:41:02 +08:00
1 parent 51d5b8a2a1
commit 024ecddfed
16 files changed
+1061 -198

No files matched your search

+212 -54
View File
@@ -1,66 +1,241 @@
/* Forum surface: home tabs, plate panels, post cards, listing, replies. */
#forum{--forum-ink:var(--key-color,#24282c);--forum-muted:var(--muted-color,#737b82);--forum-line:var(--muted-4-color,#e6e9eb);--forum-accent:var(--focus-color,#2869d1);color:var(--forum-ink)} #forum{--forum-ink:var(--key-color,#24282c);--forum-muted:var(--muted-color,#737b82);--forum-line:var(--muted-4-color,#e6e9eb);--forum-accent:var(--focus-color,#2869d1);color:var(--forum-ink)}
/* ── 通用壳(版块 / 分类 / 搜索列表沿用) ───────────────────────── */
#forum .jio-forum__wrap{width:min(1200px,calc(100% - 32px));margin:22px auto 70px} #forum .jio-forum__wrap{width:min(1200px,calc(100% - 32px));margin:22px auto 70px}
#forum .jio-forum__breadcrumbs{display:flex;gap:9px;align-items:center;color:var(--forum-muted);font-size:13px;margin-bottom:18px} #forum .jio-forum__breadcrumbs{display:flex;gap:9px;align-items:center;color:var(--forum-muted);font-size:13px;margin-bottom:18px}
#forum .jio-forum__breadcrumbs a{color:inherit} #forum .jio-forum__breadcrumbs a{color:inherit}
#forum .jio-forum__layout{display:grid;grid-template-columns:minmax(0,1fr) 286px;gap:24px;align-items:start} #forum .jio-forum__layout{display:grid;grid-template-columns:minmax(0,1fr) 286px;gap:24px;align-items:start}
#forum .jio-forum__main{min-width:0} #forum .jio-forum__main{min-width:0}
#forum .jio-forum__hero,#forum .jio-forum__page-head{padding:20px 0;border-bottom:1px solid var(--forum-line)} #forum .jio-forum__page-head{padding:20px 0;border-bottom:1px solid var(--forum-line)}
#forum h1{font-size:27px;line-height:1.25;margin:0 0 10px} #forum h1{font-size:27px;line-height:1.25;margin:0 0 10px}
#forum .jio-forum__hero p,#forum .jio-forum__page-head p{margin:0;color:var(--forum-muted)} #forum .jio-forum__page-head p{margin:0;color:var(--forum-muted)}
#forum .jio-forum__aside{border-left:1px solid var(--forum-line);padding-left:20px}
#forum .jio-forum__new,#forum .jio-forum__primary{display:inline-flex;align-items:center;justify-content:center;background:var(--forum-accent);color:#fff;border:0;border-radius:4px;min-height:40px;padding:8px 16px;cursor:pointer}
#forum .jio-forum__new{width:100%}
#forum .jio-forum__aside-section{padding:22px 0;border-bottom:1px solid var(--forum-line)}
#forum .jio-forum__aside-section h2{font-size:16px;margin:0 0 12px}
#forum .jio-forum__aside-section a{display:block;padding:6px 0;color:var(--forum-ink)}
#forum .jio-forum__widget-link{display:flex;justify-content:space-between;gap:10px;padding:7px 0;color:var(--forum-ink)}
#forum .jio-forum__widget-link span{color:var(--forum-muted)}
/* 版块 / 分类列表页的固定栏目条 */
#forum .jio-forum__tabs{display:flex;gap:26px;overflow:auto;white-space:nowrap;border-bottom:1px solid var(--forum-line);margin-bottom:16px} #forum .jio-forum__tabs{display:flex;gap:26px;overflow:auto;white-space:nowrap;border-bottom:1px solid var(--forum-line);margin-bottom:16px}
#forum .jio-forum__tabs a{padding:14px 0;color:var(--forum-muted);border-bottom:2px solid transparent} #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} #forum .jio-forum__tabs .is-active{color:var(--forum-accent);border-color:var(--forum-accent);font-weight:600}
#forum .jio-forum__chips{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}
#forum .jio-forum__chips a,#forum .jio-forum__badge,#forum .jio-forum__badges a,#forum .jio-forum__badges span{padding:4px 8px;background:#f1f5f7;color:#48717c;border-radius:4px;font-size:12px} /* ── 论坛首页:栏目 TAB ─────────────────────────────────────────── */
#forum .jio-forum__post{display:flex;gap:13px;padding:20px 2px;border-bottom:1px solid var(--forum-line)} #forum.forum-home{padding-bottom:40px}
#forum .jio-forum__avatar{flex:0 0 44px}#forum .jio-forum__avatar img{width:44px;height:44px;border-radius:50%} .forum-home .forum-home__main{min-width:0}
#forum .jio-forum__post-body{min-width:0;flex:1}#forum .jio-forum__post-top{display:flex;gap:12px;color:var(--forum-muted);font-size:12px} .forum-home-tab .fixed-wrap-nav{margin-bottom:10px}
#forum .jio-forum__post h2{font-size:17px;line-height:1.45;margin:7px 0}#forum .jio-forum__post h2 a{color:var(--forum-ink)} .forum-home-tab .tab-nav-theme{border-bottom:none;padding:0;margin:0}
#forum .jio-forum__post p{color:var(--forum-muted);font-size:14px;line-height:1.6;margin:0 0 10px} .forum-home-tab .tab-nav-theme li{padding:0 2px}
#forum .jio-forum__post-meta{display:flex;align-items:center;gap:10px;color:var(--forum-muted);font-size:12px;flex-wrap:wrap} .forum-home-tab .tab-nav-theme li > a{display:block;padding:10px 12px;font-size:16px;color:var(--muted-2-color)}
#forum .jio-forum__post-meta a{color:var(--forum-accent)} .forum-home-tab .tab-nav-theme li.active > a{color:var(--focus-color);font-weight:600}
#forum .jio-forum__aside{border-left:1px solid var(--forum-line);padding-left:20px} .forum-home-tab .tab-content > .tab-pane{display:none}
#forum .jio-forum__new,#forum .jio-forum__primary{display:inline-flex;align-items:center;justify-content:center;background:var(--forum-accent);color:#fff;border:0;border-radius:4px;min-height:40px;padding:8px 16px;cursor:pointer} .forum-home-tab .tab-content > .tab-pane.active{display:block}
#forum .jio-forum__new{width:100%}#forum .jio-forum__aside-section{padding:22px 0;border-bottom:1px solid var(--forum-line)} .forum-main-tab-content{min-height:120px}
#forum .jio-forum__aside-section h2{font-size:16px;margin:0 0 12px}#forum .jio-forum__aside-section a{display:block;padding:6px 0;color:var(--forum-ink)} .forum-more{padding:20px 0;text-align:center}
#forum .jio-forum__plates{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:16px} .forum-more.is-loading{display:none}
#forum .jio-forum__plate{display:flex;gap:13px;padding:14px;border:1px solid var(--forum-line);border-radius:6px} .forum-tab-pane > .ajax-item:last-of-type{margin-bottom:0}
#forum .jio-forum__plate-cover{width:80px;height:80px;flex:none;background:#edf2f5;border-radius:5px;overflow:hidden} /* 首页栏目是横向 TAB,覆盖通用 .fixed-wrap 的竖排侧栏导航形态 */
#forum .jio-forum__plate-cover img{width:100%;height:100%;object-fit:cover}#forum .jio-forum__plate h2{font-size:16px;margin:3px 0 8px} @media (min-width:768px){
#forum .jio-forum__plate p{font-size:13px;color:var(--forum-muted);margin:0} .forum-home-tab{--fixed-width:auto}
#forum .jio-forum__single{padding:20px 0}#forum .jio-forum__single-meta{display:flex;align-items:center;gap:10px;color:var(--forum-muted);font-size:13px} .forum-home-tab > .fixed-wrap-nav{position:static;width:auto}
#forum .jio-forum__single-meta img{border-radius:50%}#forum .jio-forum__single-head h1{margin:18px 0} .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}
}
.quick-posts-box{border-radius:var(--main-radius);padding:15px}
.quick-posts-box .title-theme{margin-bottom:10px}
.forum-home .jio-widget-box{clear:both;background-color:var(--main-bg-color);padding:15px;box-shadow:0 0 10px var(--main-shadow);border-radius:var(--main-radius);margin-bottom:20px}
.forum-home .jio-widget-box .btn-block{display:block;width:100%;text-align:center}
.forum-home .jio-widget-box .title-theme{font-size:15px;font-weight:600}
.quick-posts-form .form-control{width:100%}
.quick-posts-form select.form-control{width:auto;min-width:140px}
.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}
/* ── 版块面板 / 版块卡片 ────────────────────────────────────────── */
.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}
.panel-plate .title-theme{height:24px;display:flex;align-items:center;font-size:15px;font-weight:600}
.panel-plate .panel-title{padding:10px 0;margin:0}
.panel-plate .panel-title,
.panel-plate .panel-title > a{color:var(--key-color)}
.panel-plate .panel-title > a{text-decoration:none}
.panel-plate .plate-lists{padding:0 0 10px}
.panel-plate .fa{transition:.2s}
.panel-plate .collapsed .fa-angle-up{transform:scaleY(-100%)}
.panel-plate .follow-plate{display:inline-block;max-width:100%}
.plate-add{display:inline-flex;align-items:center;font-size:12px}
.plate-card{display:inline-block;text-align:center}
.plate-card .title{color:var(--main-color);font-size:13px;font-weight:500}
.scroll-plate .plate-card{padding:15px 10px 8px;width:115px;border-radius:var(--main-radius);box-shadow:0 0 10px var(--main-shadow);background:var(--main-bg-color)}
.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-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}
.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%}
.plate-item .item-info{display:flex;justify-content:space-between;flex-direction:column;flex:1;min-width:0}
.plate-item .count-mates,.plate-item .excerpt{margin-top:6px;color:var(--muted-color);min-height:1em}
.plate-item .count-mates{color:var(--muted-2-color);font-size:12px}
.plate-item .excerpt:hover,.forum-title a:hover{color:var(--key-color)}
.count-mates item{display:inline-block}
.count-mates icon,.count-mates text{margin-right:2px}
.plate-thumb{flex-shrink:0;display:inline-block;width:60px;height:60px;position:relative}
.forum-title{font-size:16px;padding:0;margin:0;line-height:1.5}
.btn-follow,.btn-follow:visited{display:inline-flex;align-items:center;justify-content:center;overflow:hidden;position:relative;cursor:pointer}
.active.jb-pink{--this-bg:rgba(27,145,255,.1);--this-color:#3097f8}
/* ── 帖子卡片 ──────────────────────────────────────────────────── */
.forum-posts{--this-padding:15px 20px;padding:var(--this-padding);display:flex;transition:.3s;position:relative}
.forum-posts.mini .avatar-img{--this-size:50px}
.forum-posts.manage .badge-status{right:3em}
.forum-posts + .forum-posts{border-top:1px solid var(--main-border-color)}
.forum-posts + .post_ajax_loader > .mb20,.forum-posts + .ajax-pag{padding-top:30px;border-top:1px solid var(--main-border-color)}
.forum-posts + .theme-pagination{margin-top:0}
.forum-posts .entry-info{flex-grow:1;min-width:0}
.forum-posts.mini .entry-info{max-width:calc(100% - 70px)}
.forum-posts .forum-title{font-weight:500}
.forum-posts .forum-title badge{font-weight:normal}
.forum-posts .forum-title > a,.forum-posts .content{line-height:22px}
.forum-posts .forum-title > a.text-ellipsis{line-height:2.5em;display:block;margin-top:-.5em}
.forum-posts .item-meta{color:var(--muted-color);opacity:.6;transition:opacity .15s}
.forum-posts:hover .item-meta{opacity:1}
.forum-posts .meta-right item{margin-left:15px}
.forum-posts.detail{display:block}
.forum-posts.detail .user-info + .badge-status{top:46px}
.forum-posts.detail .content{overflow:hidden}
.forum-posts.detail .content .excerpt{display:block}
.forum-posts.detail .but-plate .icon{color:var(--focus-color)}
.forum-posts.detail .action-meta{margin-right:-7px}
.forum-posts.detail .but-plate{margin:3px 0}
.forum-posts.detail .action-meta .item{color:var(--muted-color);margin-left:5px;padding:2px 8px;font-size:20px;transition:.15s;border-radius:4px;display:inline-flex;align-items:center;border:0;background:none;cursor:pointer}
.forum-posts.detail .action-meta .btn-score{font-size:20px;display:inline-flex;align-items:center;color:var(--muted-color);padding:2px;border-radius:4px}
.forum-posts.detail .action-meta > .item.active{color:var(--focus-color)}
.forum-posts.detail .action-meta text{font-size:12px;margin-left:2px;min-width:14px;color:var(--muted-color);text-align:center}
.forum-posts.detail .plate-null + .action-meta{flex:auto;justify-content:flex-end}
.forum-posts .hot-comment .avatar-img,.forum-posts.detail .hot-comment .avatar-img{--this-size:22px;margin-right:6px}
.forum-posts .hot-comment{background:var(--muted-border-color);border-radius:var(--main-radius);padding:8px 10px;font-size:13px;color:var(--muted-color)}
.forum-posts .hot-comment .comment-content img:not(.smilie-icon){max-width:200px;max-height:200px;display:block;width:auto;height:auto}
.forum-posts.alone{border:none!important;background:var(--main-bg-color);border-radius:var(--main-radius);margin-bottom:10px;--this-padding:20px 26px}
.forum-posts.alone + .post_ajax_loader > .mb20,.forum-posts.alone + .ajax-pag{border-top:none;padding:20px 0 30px;margin:0}
.forum-posts .hide-post{margin-top:6px;padding:10px 12px;background:var(--muted-border-color);border-radius:var(--main-radius);color:var(--muted-color);font-size:13px}
.forum-posts .lists-imgs{margin-top:6px}
.lists-imgs:not(.count-1){display:flex;flex-wrap:wrap;--img-count:4;max-width:650px}
.lists-imgs:not(.count-1) > span{flex:0 0 calc(100% / var(--img-count) - 4px);padding-bottom:calc(100% / var(--img-count) - 4px);margin:2px;position:relative;border-radius:4px;overflow:hidden}
.lists-imgs.count-2{--img-count:3}
.lists-imgs:not(.count-1) > span > img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}
.lists-imgs.count-1 img{max-width:300px;max-height:300px;width:auto;height:auto;border-radius:4px}
.lists-imgs-more{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;background:rgba(0,0,0,.45);color:#fff;font-size:18px;font-weight:600}
.forum-posts .badge-status{position:absolute;right:0;top:0}
#forum .badg.but-plate,#forum .but.but-plate{font-size:12px;border-radius:100px;padding:3px 10px;vertical-align:.1em;display:inline-flex;align-items:center;text-decoration:none}
.badge-question{border-radius:.45em 2px;padding:0 .25em;line-height:1.4em}
.badge-question > text{font-size:.8em;margin-left:2px;vertical-align:.05em}
/* ── 首页 / 列表响应式 ─────────────────────────────────────────── */
@media (min-width:641px){
.forum-posts.mini:hover{background:var(--muted-border-color)}
.forum-posts.detail .avatar-img{--this-size:55px;margin-right:5px}
.forum-posts.detail .user-info + *,.forum-posts.detail .user-info .user-right{margin-top:-12px}
.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)}
}
@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}
}
@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 li > a{font-size:15px;padding:8px 10px}
}
@media (max-width: 640px){
.plate-lists .plate-item{width:calc(100% - 6px);padding:15px 12px;align-items:center}
.plate-item .plate-thumb{width:40px;height:40px;margin-right:10px}
.plate-item .excerpt{display:none}
.plate-item .forum-title{font-size:13px}
.follow-plate .plate-card{width:60px}
.follow-plate .plate-card .title{font-size:12px}
.forum-posts,.forum-posts.alone{--this-padding:12px 0}
.forum-posts.mini{flex-direction:column}
.forum-posts.mini .forum-user{display:flex;align-items:center;margin-bottom:8px}
.forum-posts.mini .entry-info{max-width:100%}
.forum-posts.mini .item-meta{margin-top:6px}
.forum-posts.mini .item-meta item{margin-left:8px}
.forum-posts.mini .item-meta .meta-left{margin-left:-5px}
.forum-posts .forum-title > a.text-ellipsis{line-height:22px;font-size:15px;display:block;overflow:unset;text-overflow:unset;white-space:unset;margin:0}
.forum-posts.mini .avatar-img{--this-size:22px;margin-top:-3px}
.forum-posts.detail .action-meta > .item{margin:0}
.forum-posts.detail .user-info ~ *{margin-left:0}
.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}
#forum .jio-forum__tabs{gap:18px}
.forum-home-tab .tab-nav-theme li > a{font-size:14px;padding:8px}
.forum-posts .action-meta text{display:none}
.quick-posts-form select.form-control{width:100%}
}
/* ── 帖子详情 / 回复 / 编辑器 ──────────────────────────────────── */
#forum .jio-forum__single{padding:20px 0}
#forum .jio-forum__single-meta{display:flex;align-items:center;gap:10px;color:var(--forum-muted);font-size:13px}
#forum .jio-forum__single-meta img{border-radius:50%}
#forum .jio-forum__single-head h1{margin:18px 0}
#forum .jio-forum__badges,#forum .jio-forum__actions{display:flex;gap:8px;flex-wrap:wrap} #forum .jio-forum__badges,#forum .jio-forum__actions{display:flex;gap:8px;flex-wrap:wrap}
#forum .jio-forum__single-content{font-size:15px;line-height:1.8;padding:22px 0;overflow-wrap:anywhere} #forum .jio-forum__single-content{font-size:15px;line-height:1.8;padding:22px 0;overflow-wrap:anywhere}
#forum .jio-forum__actions{border-top:1px solid var(--forum-line);padding:16px 0} #forum .jio-forum__actions{border-top:1px solid var(--forum-line);padding:16px 0}
#forum .jio-forum__action{border:1px solid var(--forum-line);border-radius:4px;background:transparent;color:var(--forum-muted);padding:7px 12px;cursor:pointer} #forum .jio-forum__action{border:1px solid var(--forum-line);border-radius:4px;background:transparent;color:var(--forum-muted);padding:7px 12px;cursor:pointer}
#forum .jio-forum__action:hover{color:var(--forum-accent);border-color:var(--forum-accent)} #forum .jio-forum__action:hover{color:var(--forum-accent);border-color:var(--forum-accent)}
#forum .jio-forum__form{display:grid;gap:15px;margin:20px 0}#forum .jio-forum__form label{display:grid;gap:6px;font-size:14px;font-weight:600} #forum .jio-forum__form{display:grid;gap:15px;margin:20px 0}
#forum .jio-forum__form label{display:grid;gap:6px;font-size:14px;font-weight:600}
#forum .jio-forum__form input,#forum .jio-forum__form select,#forum .jio-forum__form textarea{width:100%;border:1px solid #d6dce1;border-radius:4px;padding:10px 12px;background:var(--body-bg,#fff);color:inherit;font:inherit} #forum .jio-forum__form input,#forum .jio-forum__form select,#forum .jio-forum__form textarea{width:100%;border:1px solid #d6dce1;border-radius:4px;padding:10px 12px;background:var(--body-bg,#fff);color:inherit;font:inherit}
#forum .jio-forum__form-extra[hidden]{display:none}#forum .jio-forum__message{color:#b64343;min-height:20px} #forum .jio-forum__form-extra[hidden]{display:none}
#forum .jio-forum__vote{border-top:1px solid var(--forum-line);padding:20px 0}#forum .jio-forum__vote h2{font-size:17px} #forum .jio-forum__message{color:#b64343;min-height:20px}
#forum .jio-forum__vote{border-top:1px solid var(--forum-line);padding:20px 0}
#forum .jio-forum__vote h2{font-size:17px}
#forum .jio-forum__vote label{display:flex;gap:12px;padding:12px;border:1px solid var(--forum-line);margin-bottom:8px;border-radius:4px} #forum .jio-forum__vote label{display:flex;gap:12px;padding:12px;border:1px solid var(--forum-line);margin-bottom:8px;border-radius:4px}
#forum .jio-forum__vote-count{margin-left:auto}#forum .jio-forum__locked{padding:30px;text-align:center;background:#f7f9fa} #forum .jio-forum__vote-count{margin-left:auto}
#forum .jio-forum__locked{padding:30px;text-align:center;background:#f7f9fa}
#forum .jio-forum__pagination ul{list-style:none;display:flex;gap:7px;padding:0;margin:24px 0} #forum .jio-forum__pagination ul{list-style:none;display:flex;gap:7px;padding:0;margin:24px 0}
#forum .jio-forum__pagination a,#forum .jio-forum__pagination span{display:block;padding:6px 10px;border:1px solid var(--forum-line);border-radius:4px} #forum .jio-forum__pagination a,#forum .jio-forum__pagination span{display:block;padding:6px 10px;border:1px solid var(--forum-line);border-radius:4px}
#forum .jio-forum__empty{padding:36px;text-align:center;color:var(--forum-muted)} #forum .jio-forum__empty{padding:36px;text-align:center;color:var(--forum-muted)}
@media(max-width:800px){#forum .jio-forum__layout{grid-template-columns:1fr}#forum .jio-forum__aside{border-left:0;padding-left:0}#forum .jio-forum__plates{grid-template-columns:1fr}} #forum .jio-forum__badge,#forum .jio-forum__badges a,#forum .jio-forum__badges span{padding:4px 8px;background:#f1f5f7;color:#48717c;border-radius:4px;font-size:12px}
@media(max-width:480px){#forum .jio-forum__wrap{width:calc(100% - 24px)}#forum h1{font-size:23px}#forum .jio-forum__post{padding:16px 0}#forum .jio-forum__tabs{gap:18px}}
#forum .jio-forum__create-plate{width:100%;margin-top:10px}
#forum .jio-forum__section-title{font-size:18px;margin:28px 0 0} #forum .jio-forum__section-title{font-size:18px;margin:28px 0 0}
#forum .jio-forum__reply{display:flex;gap:12px;padding:17px 0;border-bottom:1px solid var(--forum-line)} #forum .jio-forum__reply{display:flex;gap:12px;padding:17px 0;border-bottom:1px solid var(--forum-line)}
#forum .jio-forum__reply-avatar img{border-radius:50%} #forum .jio-forum__reply-avatar img{border-radius:50%}
#forum .jio-forum__reply-main{flex:1;min-width:0} #forum .jio-forum__reply-main{flex:1;min-width:0}
#forum .jio-forum__reply-head{display:flex;gap:12px;align-items:center;font-size:13px} #forum .jio-forum__reply-head{display:flex;gap:12px;align-items:center;font-size:13px}
#forum .jio-forum__reply-head time{color:var(--forum-muted)}#forum .jio-forum__reply-head strong{color:#147e70} #forum .jio-forum__reply-head time{color:var(--forum-muted)}
#forum .jio-forum__reply-head strong{color:#147e70}
#forum .jio-forum__reply-text{padding:10px 0;line-height:1.7;overflow-wrap:anywhere} #forum .jio-forum__reply-text{padding:10px 0;line-height:1.7;overflow-wrap:anywhere}
#forum .jio-forum__reply-actions button{border:0;background:none;color:var(--forum-accent);padding:4px 10px 4px 0;cursor:pointer} #forum .jio-forum__reply-actions button{border:0;background:none;color:var(--forum-accent);padding:4px 10px 4px 0;cursor:pointer}
#forum .jio-forum__reply-form{display:grid;gap:12px;margin:24px 0} #forum .jio-forum__reply-form{display:grid;gap:12px;margin:24px 0}
#forum .jio-forum__reply-form h3{font-size:17px;margin:0} #forum .jio-forum__reply-form h3{font-size:17px;margin:0}
#forum .jio-forum__reply-form textarea{width:100%;border:1px solid var(--forum-line);padding:12px;border-radius:4px;background:var(--body-bg,#fff);color:inherit} #forum .jio-forum__reply-form textarea{width:100%;border:1px solid var(--forum-line);padding:12px;border-radius:4px;background:var(--body-bg,#fff);color:inherit}
#forum .jio-forum__reply-form button{justify-self:start} #forum .jio-forum__reply-form button{justify-self:start}
#forum .jio-forum__widget-link{display:flex;justify-content:space-between;gap:10px;padding:7px 0;color:var(--forum-ink)} #forum .jio-forum__reply-parent{color:var(--forum-muted);font-size:12px;margin:8px 0 0}
#forum .jio-forum__widget-link span{color:var(--forum-muted)} #forum .jio-forum__editor-tools{display:flex;gap:8px;flex-wrap:wrap}
#forum .wp-editor-wrap{border:1px solid var(--forum-line)}
#forum .jio-forum__applications{margin:18px 0;padding:14px 0;border-top:1px solid var(--forum-line)}
#forum .jio-forum__applications h2{font-size:16px;margin:0 0 8px}
#forum .jio-forum__application{display:flex;align-items:center;gap:10px;padding:6px 0}
#forum .jio-forum__application button{border:1px solid var(--forum-line);border-radius:4px;background:transparent;padding:4px 9px;cursor:pointer}
/* ── 弹窗 / 用户面板 ──────────────────────────────────────────── */
.jio-forum__dialog{width:min(440px,calc(100% - 28px));max-height:90vh;border:1px solid #dce2e5;border-radius:6px;padding:22px;background:var(--body-bg,#fff);color:inherit;box-shadow:0 20px 60px #0003} .jio-forum__dialog{width:min(440px,calc(100% - 28px));max-height:90vh;border:1px solid #dce2e5;border-radius:6px;padding:22px;background:var(--body-bg,#fff);color:inherit;box-shadow:0 20px 60px #0003}
.jio-forum__dialog::backdrop{background:#10182088} .jio-forum__dialog::backdrop{background:#10182088}
.jio-forum__dialog h2{font-size:20px;margin:0 0 18px;overflow-wrap:anywhere} .jio-forum__dialog h2{font-size:20px;margin:0 0 18px;overflow-wrap:anywhere}
@@ -72,29 +247,12 @@
.jio-forum__qr{width:180px;height:180px;margin:auto} .jio-forum__qr{width:180px;height:180px;margin:auto}
.jio-forum__user-panel h2{font-size:18px;margin:0 0 14px} .jio-forum__user-panel h2{font-size:18px;margin:0 0 14px}
.jio-forum__user-row{display:flex;justify-content:space-between;gap:12px;padding:12px 0;border-bottom:1px solid #e6e9eb} .jio-forum__user-row{display:flex;justify-content:space-between;gap:12px;padding:12px 0;border-bottom:1px solid #e6e9eb}
#forum .jio-forum__applications{margin:18px 0;padding:14px 0;border-top:1px solid var(--forum-line)}
#forum .jio-forum__applications h2{font-size:16px;margin:0 0 8px} /* ── 深色主题 ─────────────────────────────────────────────────── */
#forum .jio-forum__application{display:flex;align-items:center;gap:10px;padding:6px 0} body.dark-theme #forum .jio-forum__badge,body.dark-theme #forum .jio-forum__badges a,body.dark-theme #forum .jio-forum__badges span{background:var(--muted-bg-2-color);color:var(--forum-accent)}
#forum .jio-forum__application button{border:1px solid var(--forum-line);border-radius:4px;background:transparent;padding:4px 9px;cursor:pointer} body.dark-theme #forum .jio-forum__locked,body.dark-theme #forum .forum-posts .hot-comment,body.dark-theme #forum .forum-posts .hide-post{background:var(--muted-bg-2-color)}
#forum .jio-forum__reply-parent{color:var(--forum-muted);font-size:12px;margin:8px 0 0}
#forum .jio-forum__hot-reply{margin:0 0 10px 58px;padding:9px 12px;background:#f5f8f9;color:var(--forum-muted);font-size:13px;border-radius:4px}
#forum .jio-forum__pending{padding:16px 0;border-bottom:1px solid var(--forum-line)}
#forum .jio-forum__pending h2{font-size:16px;margin:0 0 10px}
#forum .jio-forum__pending>div{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:6px 0}
#forum .jio-forum__pending button{border:1px solid var(--forum-line);background:transparent;border-radius:4px;padding:5px 9px;cursor:pointer}
#forum .jio-forum__editor-tools{display:flex;gap:8px;flex-wrap:wrap}
#forum .wp-editor-wrap{border:1px solid var(--forum-line)}
body.dark-theme #forum .jio-forum__chips a,body.dark-theme #forum .jio-forum__badge,body.dark-theme #forum .jio-forum__badges a,body.dark-theme #forum .jio-forum__badges span{background:var(--muted-bg-2-color);color:var(--forum-accent)}
body.dark-theme #forum .jio-forum__plate-cover{background:var(--muted-bg-2-color)}
body.dark-theme #forum .jio-forum__locked,body.dark-theme #forum .jio-forum__hot-reply{background:var(--muted-bg-2-color)}
body.dark-theme #forum .jio-forum__form input,body.dark-theme #forum .jio-forum__form select,body.dark-theme #forum .jio-forum__form textarea,body.dark-theme #forum .jio-forum__reply-form textarea{background:var(--main-bg-color);border-color:var(--forum-line);color:var(--forum-ink)} body.dark-theme #forum .jio-forum__form input,body.dark-theme #forum .jio-forum__form select,body.dark-theme #forum .jio-forum__form textarea,body.dark-theme #forum .jio-forum__reply-form textarea{background:var(--main-bg-color);border-color:var(--forum-line);color:var(--forum-ink)}
body.dark-theme .jio-forum__dialog{background:var(--main-bg-color);color:var(--key-color);border-color:var(--muted-4-color)} body.dark-theme .jio-forum__dialog{background:var(--main-bg-color);color:var(--key-color);border-color:var(--muted-4-color)}
body.dark-theme .jio-forum__dialog input,body.dark-theme .jio-forum__dialog select,body.dark-theme .jio-forum__dialog textarea{background:var(--main-bg-color);color:var(--key-color);border-color:var(--muted-4-color)} body.dark-theme .jio-forum__dialog input,body.dark-theme .jio-forum__dialog select,body.dark-theme .jio-forum__dialog textarea{background:var(--main-bg-color);color:var(--key-color);border-color:var(--muted-4-color)}
body.dark-theme #forum .jio-forum__action{color:var(--forum-muted)} body.dark-theme #forum .jio-forum__action{color:var(--forum-muted)}
#forum .jio-forum__term-management{margin:16px 0;padding:16px 0;border-bottom:1px solid var(--forum-line)} body.dark-theme .panel.panel-plate,body.dark-theme .plate-lists .plate-item,body.dark-theme .forum-posts.alone,body.dark-theme .scroll-plate .plate-card{background:var(--main-bg-color)}
#forum .jio-forum__term-management h2{font-size:16px}
#forum .jio-forum__term-management form{display:flex;gap:8px;align-items:end;flex-wrap:wrap}
#forum .jio-forum__term-management label{display:grid;gap:5px;font-size:13px}
#forum .jio-forum__term-management input,#forum .jio-forum__term-management select{min-height:34px;padding:5px 8px;border:1px solid var(--forum-line);border-radius:4px;background:var(--main-bg-color);color:var(--forum-ink)}
#forum .jio-forum__term-row{display:flex;gap:8px;align-items:center;padding:5px 0}
#forum .jio-forum__term-row button{border:1px solid var(--forum-line);border-radius:4px;background:transparent;padding:4px 7px;cursor:pointer}
+102 -15
View File
@@ -23,8 +23,7 @@
else if (value) window.alert(value) else if (value) window.alert(value)
} }
const modal = (title, fields, submitText = '确认') => new Promise((resolve) => { const modal = (title, fields, submitText = '确认') => new Promise((resolve) => { const dialog = document.createElement('dialog')
const dialog = document.createElement('dialog')
dialog.className = 'jio-forum__dialog' dialog.className = 'jio-forum__dialog'
const form = document.createElement('form') const form = document.createElement('form')
form.innerHTML = '<h2></h2><div class="jio-forum__dialog-fields"></div><div class="jio-forum__dialog-actions"><button type="button" data-cancel>取消</button><button type="submit" class="jio-forum__primary"></button></div>' form.innerHTML = '<h2></h2><div class="jio-forum__dialog-fields"></div><div class="jio-forum__dialog-actions"><button type="button" data-cancel>取消</button><button type="submit" class="jio-forum__primary"></button></div>'
@@ -172,7 +171,13 @@
return return
} }
const open = event.target.closest('[data-forum-open]') const open = event.target.closest('[data-forum-open]')
if (open) { root.querySelector(`[data-forum-dialog="${open.dataset.forumOpen}"]`)?.showModal(); return } if (open) {
const dialog = root.querySelector(`[data-forum-dialog="${open.dataset.forumOpen}"]`)
const category = dialog?.querySelector('select[name="category_id"]')
if (category && open.dataset.category) category.value = open.dataset.category
dialog?.showModal()
return
}
const close = event.target.closest('[data-forum-close]') const close = event.target.closest('[data-forum-close]')
if (close) { close.closest('dialog')?.close(); return } if (close) { close.closest('dialog')?.close(); return }
const reply = event.target.closest('[data-forum-reply-to]') const reply = event.target.closest('[data-forum-reply-to]')
@@ -193,7 +198,7 @@
if (action === 'plate-unlock') { await checkout('plate', id); return } if (action === 'plate-unlock') { await checkout('plate', id); return }
if (action === 'plate-audit') { if (action === 'plate-audit') {
result = await send(`plates/${id}/audit`, 'POST', { status: 'publish' }) result = await send(`plates/${id}/audit`, 'POST', { status: 'publish' })
button.closest('.jio-forum__pending > div')?.remove() button.closest('.forum-pending-plate')?.remove()
} else if (action === 'plate-delete') { } else if (action === 'plate-delete') {
const data = await modal('删除版块', [ const data = await modal('删除版块', [
{ name: 'posts_action', label: '关联帖子', type: 'select', options: [{ value: 'trash', label: '一并移入回收站' }, { value: 'move', label: '移动到其他版块' }] }, { name: 'posts_action', label: '关联帖子', type: 'select', options: [{ value: 'trash', label: '一并移入回收站' }, { value: 'move', label: '移动到其他版块' }] },
@@ -213,16 +218,6 @@
if (!window.confirm('确定删除此回复?')) return if (!window.confirm('确定删除此回复?')) return
result = await send(`replies/${id}/delete`, 'POST') result = await send(`replies/${id}/delete`, 'POST')
button.closest('.jio-forum__reply')?.remove() button.closest('.jio-forum__reply')?.remove()
} else if (action === 'term-edit') {
const name = button.closest('.jio-forum__term-row')?.querySelector('span')?.textContent || ''
const data = await modal('编辑分类', [{ name: 'name', label: '名称', value: name }], '保存')
if (!data) return
result = await send(`terms/${id}`, 'PATCH', data)
button.closest('.jio-forum__term-row').querySelector('span').textContent = data.name
} else if (action === 'term-delete') {
if (!window.confirm('确定删除这个分类?')) return
result = await send(`terms/${id}`, 'DELETE')
button.closest('.jio-forum__term-row')?.remove()
} else if (action === 'follow') { } else if (action === 'follow') {
try { result = await send(`plates/${id}/follow`, 'POST') } try { result = await send(`plates/${id}/follow`, 'POST') }
catch (error) { if (error.message.includes('付费')) { await checkout('plate', id); return } throw error } catch (error) { if (error.message.includes('付费')) { await checkout('plate', id); return } throw error }
@@ -254,7 +249,17 @@
button.dataset.forumAction = 'restore' button.dataset.forumAction = 'restore'
button.textContent = '撤销删除' button.textContent = '撤销删除'
} else if (action === 'adopt' || action === 'audit') window.location.reload() } else if (action === 'adopt' || action === 'audit') window.location.reload()
else button.textContent = result.data?.label || result.message || button.textContent else if (action === 'follow') {
const following = result.data?.following === true
button.classList.toggle('active', following)
const label = button.querySelector('text')
if (label) label.textContent = following ? '已关注' : '关注'
else button.textContent = result.data?.label || button.textContent
} else if (button.querySelector('text')) {
button.querySelector('text').textContent = result.data?.label || result.message || button.querySelector('text').textContent
} else {
button.textContent = result.data?.label || result.message || button.textContent
}
message(button, result.message || '') message(button, result.message || '')
} catch (error) { } catch (error) {
message(button, error.message) message(button, error.message)
@@ -263,4 +268,86 @@
button.disabled = false button.disabled = false
} }
}) })
// 论坛首页栏目:点击切换 + 懒加载该栏目的 ajaxpager 内容
const refresh = (pane) => {
if (typeof window.auto_fun === 'function') window.auto_fun()
if (typeof window.jioInitForumMedia === 'function') window.jioInitForumMedia(pane)
}
const loadTab = async (pane, href) => {
if (!pane || pane.dataset.loaded === '1' || pane.dataset.loading === '1') return
pane.dataset.loading = '1'
pane.querySelector('.post_ajax_loader')?.classList.add('is-loading')
try {
const response = await fetch(href, { credentials: 'same-origin', headers: { 'X-Requested-With': 'XMLHttpRequest' } })
if (!response.ok) throw new Error('加载失败')
const doc = new DOMParser().parseFromString(await response.text(), 'text/html')
const source = doc.getElementById(pane.id)
if (!source) throw new Error('内容不存在')
pane.innerHTML = source.innerHTML
pane.dataset.loaded = '1'
refresh(pane)
} catch (error) {
pane.querySelector('.post_ajax_loader')?.classList.remove('is-loading')
message(pane, error.message)
} finally {
pane.dataset.loading = ''
}
}
const loadMore = async (link, pane) => {
if (pane.dataset.loading === '1') return
pane.dataset.loading = '1'
const more = link.closest('.forum-more')
const loader = [...pane.children].find((item) => item.classList.contains('post_ajax_loader'))
more?.classList.add('is-loading')
loader?.classList.add('is-loading')
try {
const response = await fetch(link.getAttribute('href'), { credentials: 'same-origin', headers: { 'X-Requested-With': 'XMLHttpRequest' } })
if (!response.ok) throw new Error('加载失败')
const doc = new DOMParser().parseFromString(await response.text(), 'text/html')
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'))
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'))
else anchor?.remove()
refresh(pane)
} catch (error) {
message(pane, error.message)
} finally {
pane.dataset.loading = ''
more?.classList.remove('is-loading')
loader?.classList.remove('is-loading')
}
}
root.addEventListener('click', (event) => {
const tab = event.target.closest('[data-forum-tab]')
if (tab) {
event.preventDefault()
const target = tab.getAttribute('data-forum-tab')
const pane = root.querySelector(target)
root.querySelectorAll('[data-forum-tab]').forEach((link) => link.closest('li')?.classList.toggle('active', link === tab))
root.querySelectorAll('.forum-tab-pane').forEach((item) => {
const current = `#${item.id}` === target
item.classList.toggle('active', current)
item.classList.toggle('in', current)
})
if (pane) {
loadTab(pane, pane.dataset.href || tab.getAttribute('href'))
if (pane.dataset.href && window.history?.replaceState) window.history.replaceState(null, document.title, pane.dataset.href)
}
return
}
const more = event.target.closest('[data-forum-more]')
if (more) {
event.preventDefault()
const pane = more.closest('.forum-tab-pane')
if (pane) loadMore(more, pane)
}
})
})() })()
+3 -3
View File
@@ -132,12 +132,12 @@ if (is_active_sidebar('all_top_fluid') || is_active_sidebar('search_top_fluid'))
} }
?> ?>
<?php elseif ($search_type === 'forum' && $wp_query->have_posts()) : ?> <?php elseif ($search_type === 'forum' && $wp_query->have_posts()) : ?>
<div id="forum" class="jio-forum"><div class="jio-forum__list"> <div id="forum" class="jio-forum forum-search"><div class="jio-forum__list">
<?php while ($wp_query->have_posts()) : $wp_query->the_post(); \Jio\Forum\View::post_card(get_post()); endwhile; wp_reset_postdata(); ?> <?php \Jio\Forum\Cards::prime($wp_query->posts); while ($wp_query->have_posts()) : $wp_query->the_post(); \Jio\Forum\View::post_card(get_post()); endwhile; wp_reset_postdata(); ?>
</div></div> </div></div>
<?php jio_render_pagination($wp_query); ?> <?php jio_render_pagination($wp_query); ?>
<?php elseif ($search_type === 'plate' && $wp_query->have_posts()) : ?> <?php elseif ($search_type === 'plate' && $wp_query->have_posts()) : ?>
<div id="forum" class="jio-forum"><div class="jio-forum__plates"> <div id="forum" class="jio-forum forum-search"><div class="plate-lists">
<?php while ($wp_query->have_posts()) : $wp_query->the_post(); \Jio\Forum\View::plate_card(get_post()); endwhile; wp_reset_postdata(); ?> <?php while ($wp_query->have_posts()) : $wp_query->the_post(); \Jio\Forum\View::plate_card(get_post()); endwhile; wp_reset_postdata(); ?>
</div></div> </div></div>
<?php jio_render_pagination($wp_query); ?> <?php jio_render_pagination($wp_query); ?>
+37 -1
View File
@@ -17,7 +17,41 @@ final class SchemaForum {
]), ]),
'home' => self::section('home', '首页与列表', [ 'home' => self::section('home', '首页与列表', [
self::field('jio_forum_page_size', '每页帖子数', 'number', 20, ['min' => 1, 'max' => 50]), self::field('jio_forum_page_size', '每页帖子数', 'number', 20, ['min' => 1, 'max' => 50]),
self::field('jio_forum_home_tabs', '首页栏目', 'checkbox', ['latest', 'popular', 'essence', 'question', 'vote', 'plates', 'follow'], ['options' => self::options(['latest' => '最新', 'popular' => '热门', 'essence' => '精华', 'question' => '问答', 'vote' => '投票', 'plates' => '版块', 'follow' => '已关注'])]), self::field('jio_forum_home_tab_items', '首页栏目', 'repeater', \Jio\Forum\HomeTabs::defaults(), [
'addText' => '添加栏目',
'description' => '选择并排序论坛首页的栏目,访客可通过 index 参数直接打开某个栏目',
'fields' => [
self::field('title', '栏目标题', 'input', ''),
self::field('type', '栏目类型', 'select', 'posts', ['options' => self::options([
'follow' => '关注',
'synthesis' => '综合',
'plate' => '版块',
'posts' => '帖子列表',
])]),
self::field('orderby', '排序方式', 'select', 'date', ['options' => self::options([
'date' => '发布时间',
'modified' => '修改时间',
'last_reply' => '最新回复',
'popular' => '热度',
'views' => '阅读量',
'score' => '评分',
])]),
self::field('filter', '内容筛选', 'select', '', ['options' => self::options([
'' => '全部',
'essence' => '精华',
'question' => '问答',
'vote' => '投票',
])]),
self::field('style', '列表样式', 'radio', 'detail', ['options' => self::options(['detail' => '详细内容', 'mini' => '简约风格'])]),
self::field('show', '显示端', 'checkbox', ['pc_s', 'm_s'], ['options' => self::options(['pc_s' => 'PC 端', 'm_s' => '移动端'])]),
],
]),
self::field('jio_forum_home_tab_active', '默认栏目', 'number', 2, ['min' => 1, 'max' => 20, 'description' => '打开首页时默认显示第几个栏目']),
self::field('jio_forum_home_quick_post', '快速发布', 'switcher', true),
self::field('jio_forum_home_quick_post_tab', '快速发布所在栏目', 'number', 2, ['min' => 1, 'max' => 20]),
self::field('jio_forum_home_hot_plate', '版块栏目显示热门推荐', 'switcher', true),
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_show_plate_cover', '显示版块封面', 'switcher', true),
self::field('jio_forum_search_enabled', '搜索包含帖子', 'switcher', true), self::field('jio_forum_search_enabled', '搜索包含帖子', 'switcher', true),
]), ]),
@@ -65,6 +99,8 @@ final class SchemaForum {
return ['key' => $key, 'title' => $title, 'version' => '1.0.0', 'option_key' => 'jio_settings', 'groups' => [['key' => $key, 'fields' => $fields]]]; return ['key' => $key, 'title' => $title, 'version' => '1.0.0', 'option_key' => 'jio_settings', 'groups' => [['key' => $key, 'fields' => $fields]]];
} }
/** Initial tab rows for the forum home; mirrors the built-in home layout. */
private static function field(string $key, string $label, string $type, $default, array $extra = []): array { private static function field(string $key, string $label, string $type, $default, array $extra = []): array {
return array_merge(['key' => $key, 'label' => $label, 'type' => $type, 'default' => $default], $extra); return array_merge(['key' => $key, 'label' => $label, 'type' => $type, 'default' => $default], $extra);
} }
+1
View File
@@ -45,6 +45,7 @@ class Templates {
'templates/pages/page-submit.php' => __('Jio - 写文章/投稿', 'jio'), 'templates/pages/page-submit.php' => __('Jio - 写文章/投稿', 'jio'),
'templates/pages/page-posts-navs.php' => __('Jio - 文章导航', 'jio'), 'templates/pages/page-posts-navs.php' => __('Jio - 文章导航', 'jio'),
'templates/pages/page-friends.php' => __('Jio - 友情链接', 'jio'), 'templates/pages/page-friends.php' => __('Jio - 友情链接', 'jio'),
'templates/pages/page-forums.php' => __('Jio - 论坛首页', 'jio'),
]; ];
} }
+215
View File
@@ -0,0 +1,215 @@
<?php
namespace Jio\Forum;
defined('ABSPATH') || exit;
final class Cards {
/** @var array<int, \WP_Comment> */
private static array $hot = [];
/** Prime authors, plates and hot replies for a page of posts in batch queries. */
public static function prime(array $posts): void {
$ids = $authors = $plates = [];
foreach ($posts as $post) {
if (!$post instanceof \WP_Post) continue;
$ids[] = $post->ID;
$authors[] = (int) $post->post_author;
$plates[] = (int) get_post_meta($post->ID, '_jio_forum_plate_id', true);
}
if (!$ids) return;
cache_users(array_values(array_unique(array_filter($authors))));
_prime_post_caches(array_values(array_unique(array_filter($plates))), true, true);
$wanted = array_flip($ids);
$comments = get_comments(['status' => 'approve', 'meta_key' => '_jio_forum_hot', 'meta_value' => '1', 'number' => 200, 'orderby' => 'comment_date_gmt', 'order' => 'DESC']);
foreach ($comments as $comment) {
$post_id = (int) $comment->comment_post_ID;
if (isset($wanted[$post_id]) && !isset(self::$hot[$post_id])) self::$hot[$post_id] = $comment;
}
}
public static function post_detail(\WP_Post $post, string $class = 'alone ajax-item', bool $show_plate = true, bool $show_topping = false): string {
$id = $post->ID;
$can_view = Access::can_view(get_current_user_id(), $id);
$plate_id = (int) get_post_meta($id, '_jio_forum_plate_id', true);
$plate = $show_plate ? self::plate_button($plate_id) : '';
$html = '<article class="forum-posts detail ' . esc_attr($class) . '">';
$html .= self::status_badge($post);
$html .= self::author_box($post);
$html .= $can_view ? self::cover($post) : '';
$html .= '<div>' . self::title($post, 'forum-title flex ac', '', $show_topping) . '</div>';
$html .= self::content($post, $can_view);
$html .= '<div class="flex ac jsb mt10">' . ($plate !== '' ? $plate : '<span class="plate-null"></span>') . self::action_meta($post) . '</div>';
return $html . '</article>';
}
public static function post_mini(\WP_Post $post, string $class = 'alone ajax-item', bool $show_topping = false): string {
$author = (int) $post->post_author;
$name = self::author_link($author);
$html = '<article class="forum-posts mini ' . esc_attr($class) . '">';
$html .= self::status_badge($post);
$html .= '<div class="mr20 forum-user">' . self::avatar($author, 'avatar-img forum-avatar') . '<span class="show-sm ml6 flex ac" style="width: 90%;">' . $name . '</span></div>';
$html .= '<div class="entry-info">' . self::title($post, 'forum-title flex ac', 'text-ellipsis', $show_topping);
$html .= '<div class="flex ac jsb item-meta"><div class="meta-left em09-sm flex"><span class="hide-sm">' . $name . '</span>';
$html .= '<span class="icon-circle" title="' . esc_attr(sprintf('发布时间:%s', get_the_date('Y-m-d H:i:s', $post))) . '">' . esc_html(self::ago($post->post_date) . '发布') . '</span></div>';
$html .= '<div class="meta-right">' . self::icon_metas($post) . '</div></div></div>';
return $html . '</article>';
}
public static function title(\WP_Post $post, string $class, string $link_class = '', bool $show_topping = false): string {
$id = $post->ID;
$badges = '';
if ($show_topping && get_post_meta($id, '_jio_forum_pinned', true)) $badges .= '<span class="badg badg-sm c-blue mr3">置顶</span>';
if (get_post_meta($id, '_jio_forum_essence', true)) $badges .= '<span class="badg badg-sm jb-red mr3">精华</span>';
$type = (string) get_post_meta($id, '_jio_forum_type', true);
if ($type === 'question') {
$solved = (bool) get_post_meta($id, '_jio_forum_adopted_comment', true);
$badges .= '<span class="badg badg-sm badge-question mr3 ' . ($solved ? 'c-green' : 'c-yellow') . '">问<text>' . ($solved ? '已解决' : '待解决') . '</text></span>';
} elseif ($type === 'vote') {
$badges .= '<span class="badg badg-sm c-blue mr3">投票</span>';
}
$url = $post->post_status === 'publish' ? get_permalink($post) : Core::edit_url($id);
$link_class = $link_class !== '' ? ' class="' . esc_attr($link_class) . '"' : '';
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);
$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);
$html = '<div class="plate-item flex">';
if (jio_get_setting('jio_forum_show_plate_cover', true)) {
$html .= '<a class="plate-thumb" href="' . esc_url($url) . '">' . self::thumbnail($plate) . '</a>';
}
$html .= '<div class="item-info flex1"><div class="info-header"><h2 class="forum-title"><a class="flex ac" href="' . esc_url($url) . '" title="' . esc_attr($plate->post_title) . '"><span class="mr6 text-ellipsis">' . esc_html($plate->post_title) . '</span>' . $today . '</a></h2></div>';
$html .= '<a class="excerpt em09" href="' . esc_url($url) . '">' . esc_html($excerpt) . '</a>';
$html .= '<div class="count-mates px12"><item><text>帖子</text>' . esc_html(jio_cut_count($counts['posts'])) . '</item><item class="ml10"><text>关注</text>' . esc_html(jio_cut_count((int) get_post_meta($plate->ID, '_jio_forum_follow_count', true))) . '</item></div>';
return $html . '</div></div>';
}
public static function plate_card(\WP_Post $plate, string $class = '', bool $follow = true, bool $today = false): string {
$url = get_permalink($plate);
$counts = PlateStats::get($plate->ID);
$spot = $today && $counts['today'] > 0 ? '<badge class="spot abs-tr" title="' . esc_attr(sprintf('今日有%d篇新帖', $counts['today'])) . '"></badge>' : '';
$html = '<div class="plate-card' . ($class !== '' ? ' ' . esc_attr($class) : '') . '"><a title="' . esc_attr($plate->post_title) . '" href="' . esc_url($url) . '">';
$html .= '<div class="plate-thumb">' . self::thumbnail($plate) . $spot . '</div>';
$html .= '<div class="title text-ellipsis mt10">' . esc_html($plate->post_title) . '</div>';
$html .= '<div class="mt3 px12 muted-2-color text-ellipsis count">' . esc_html(sprintf('%s关注', jio_cut_count((int) get_post_meta($plate->ID, '_jio_forum_follow_count', true)))) . '</div></a>';
if ($follow) $html .= self::follow_button($plate->ID, 'but jb-pink but-plate btn-block mt20');
return $html . '</div>';
}
/** @param \WP_Post[] $plates */
public static function plate_slide(array $plates, string $class = '', bool $follow = true, bool $today = false): string {
if (!$plates) return '';
$slides = '';
foreach ($plates as $plate) {
$slides .= '<div class="swiper-slide">' . self::plate_card($plate, $class, $follow, $today) . '</div>';
}
return '<div class="swiper-container swiper-scroll scroll-plate"><div class="swiper-wrapper">' . $slides . '</div><div class="swiper-button-prev"></div><div class="swiper-button-next"></div></div>';
}
public static function follow_button(int $plate_id, string $class): string {
if (!is_user_logged_in()) {
return '<a class="btn-follow ' . esc_attr($class) . '" href="' . esc_url(jio_get_login_url(get_permalink($plate_id))) . '"><text>关注</text></a>';
}
$followed = Access::follows(get_current_user_id(), $plate_id);
return '<button type="button" class="btn-follow ' . esc_attr($class . ($followed ? ' active' : '')) . '" data-forum-action="follow" data-id="' . (int) $plate_id . '"><text>' . ($followed ? '已关注' : '关注') . '</text></button>';
}
public static function plate_button(int $plate_id): string {
if ($plate_id <= 0 || get_queried_object_id() === $plate_id || get_post_status($plate_id) !== 'publish') return '';
return '<a class="but but-plate text-ellipsis" href="' . esc_url(get_permalink($plate_id)) . '"><i class="fas fa-cube mr3"></i>' . esc_html(get_the_title($plate_id)) . '<i class="fas fa-angle-right ml6"></i></a>';
}
public static function avatar(int $user_id, string $class): string {
return function_exists('jio_get_avatar_box') ? jio_get_avatar_box($user_id, $class) : '<span class="' . esc_attr($class) . '">' . get_avatar($user_id, 80) . '</span>';
}
public static function ago(string $date): string {
return function_exists('jio_time_ago') ? jio_time_ago($date) : human_time_diff((int) strtotime($date), (int) current_time('timestamp')) . '前';
}
private static function author_link(int $user_id): string {
return '<a class="display-name text-ellipsis" href="' . esc_url(get_author_posts_url($user_id)) . '">' . esc_html((string) get_the_author_meta('display_name', $user_id)) . '</a>';
}
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 = '<span title="' . esc_attr(get_the_date('Y-m-d H:i:s', $post)) . '">' . esc_html(self::ago($post->post_date)) . '</span><span class="icon-spot">' . esc_html(sprintf('%s次阅读', $views)) . '</span>';
return '<div class="user-info flex ac mb10">' . self::avatar($author, 'avatar-img') . '<div class="user-right flex flex1 ac jsb ml10"><div class="flex1 overflow-hidden"><div class="flex ac">' . $name . '</div><div class="px12-sm muted-2-color text-ellipsis mt3">' . $desc . '</div></div></div></div>';
}
private static function status_badge(\WP_Post $post): string {
$labels = ['pending' => '待审核', 'draft' => '草稿', 'trash' => '已删除'];
return isset($labels[$post->post_status]) ? '<span class="img-badge badge-status top jb-yellow px12">' . esc_html($labels[$post->post_status]) . '</span>' : '';
}
private static function cover(\WP_Post $post): string {
if (!has_post_thumbnail($post)) return '';
return '<div class="forum-lists-cover mb10"><div class="graphic imgbox-container" style="padding-bottom:45%">' . get_the_post_thumbnail($post, 'large', ['class' => 'fit-cover']) . '</div></div>';
}
private static function content(\WP_Post $post, bool $can_view): string {
if (!$can_view) {
return '<div class="hide-post mt6"><i class="fa fas fa-unlock-alt mr6"></i>此处内容已隐藏</div>';
}
$url = $post->post_status === 'publish' ? get_permalink($post) : Core::edit_url($post->ID);
$images = self::images($post);
$text = trim((string) preg_replace('/\s+/u', ' ', wp_strip_all_tags(strip_shortcodes($post->post_excerpt ?: $post->post_content))));
$text = jio_str_cut($text, 0, $images !== '' ? 60 : 120);
$html = '<div><div class="content mt6">' . ($text !== '' ? '<a href="' . esc_url($url) . '" class="excerpt">' . esc_html($text) . '</a>' : '') . '</div>';
$html .= $images;
$hot = self::$hot[$post->ID] ?? null;
if ($hot instanceof \WP_Comment) {
$html .= '<div class="hot-comment mt10"><div class="flex ac mb6">' . self::avatar((int) $hot->user_id, 'avatar-img') . '<span class="muted-color em09">' . esc_html(get_comment_author($hot)) . '</span><span class="badg badg-sm c-red ml6">热门回复</span></div>';
$html .= '<div class="comment-content">' . esc_html(wp_trim_words(wp_strip_all_tags($hot->comment_content), 40)) . '</div></div>';
}
return $html . '</div>';
}
private static function images(\WP_Post $post): string {
if (!preg_match_all('/<img[^>]+src=["\']([^"\']+)["\']/i', $post->post_content, $matches)) return '';
$urls = array_values(array_unique(array_filter(array_map('esc_url_raw', $matches[1]))));
$total = count($urls);
if ($total === 0) return '';
$shown = min(4, $total);
$html = '<div class="imgbox-container lists-imgs mt6 count-' . $shown . '">';
foreach (array_slice($urls, 0, $shown) as $index => $url) {
$more = $index === $shown - 1 && $total > $shown ? '<span class="abs-center lists-imgs-more">+' . ($total - $shown) . '</span>' : '';
$html .= '<span><img' . jio_get_lazy_attr('lazy_posts_content', $url, 'fit-cover radius4') . ' alt="' . esc_attr($post->post_title) . '">' . $more . '</span>';
}
return $html . '</div>';
}
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 = '<div class="action-meta flex0 flex jsb">';
if (jio_get_setting('jio_forum_score_enabled', true)) {
$html .= is_user_logged_in()
? '<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($score ? jio_cut_count($score) : '加分') . '</text></button>'
: '<a class="item btn-score" href="' . esc_url(jio_get_login_url(get_permalink($post))) . '"><i class="fa far fa-thumbs-up"></i><text>' . esc_html($score ? jio_cut_count($score) : '加分') . '</text></a>';
}
$html .= '<a rel="nofollow" href="' . esc_url(get_permalink($post) . '#comments') . '" class="item"><i class="fa far fa-comment"></i><text>' . esc_html($reply ? jio_cut_count($reply) : '回复') . '</text></a>';
if ($post->post_status === 'publish' && function_exists('jio_get_post_share_btn')) {
$html .= jio_get_post_share_btn($post, 'item btn-share', true, false);
}
return $html . '</div>';
}
private static function icon_metas(\WP_Post $post): string {
$id = $post->ID;
return '<item><i class="fa far fa-eye mr3"></i>' . esc_html(jio_cut_count((int) get_post_meta($id, '_jio_forum_views', true))) . '</item>'
. '<item><i class="fa far fa-comment mr3"></i>' . esc_html(jio_cut_count((int) $post->comment_count)) . '</item>'
. '<item><i class="fa far fa-thumbs-up mr3"></i>' . esc_html(jio_cut_count((int) get_post_meta($id, '_jio_forum_score', true))) . '</item>';
}
private static function thumbnail(\WP_Post $plate): string {
$src = get_the_post_thumbnail_url($plate, 'thumbnail') ?: get_template_directory_uri() . '/assets/img/thumbnail.svg';
return '<img' . jio_get_lazy_attr('lazy_other', (string) $src, 'forum-thumbnail fit-cover') . ' alt="' . esc_attr($plate->post_title) . '">';
}
}
+8
View File
@@ -28,6 +28,14 @@ final class Core {
add_action('trashed_post', [self::class, 'cascade_trashed_plate']); add_action('trashed_post', [self::class, 'cascade_trashed_plate']);
add_action('untrashed_post', [self::class, 'restore_trashed_plate']); add_action('untrashed_post', [self::class, 'restore_trashed_plate']);
add_filter('wp_untrash_post_status', [self::class, 'restore_status'], 10, 2); add_filter('wp_untrash_post_status', [self::class, 'restore_status'], 10, 2);
add_filter('jio_is_show_sidebar', [self::class, 'show_home_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;
$cfg = function_exists('jio_get_sidebar_config_safe') ? jio_get_sidebar_config_safe() : [];
return !empty($cfg['enabled']);
} }
public static function enabled(): bool { public static function enabled(): bool {
+196
View File
@@ -0,0 +1,196 @@
<?php
namespace Jio\Forum;
defined('ABSPATH') || exit;
final class HomeContent {
public static function render(array $tab, int $index): string {
$page = self::page();
$html = match ($tab['type']) {
'plate' => self::plate_tab(),
'follow' => self::follow_tab($tab, $index, $page),
'pending' => self::pending_tab($tab, $index, $page),
default => self::posts_tab($tab, $index, $page),
};
if ($page === 1 && $tab['type'] !== 'plate' && jio_get_setting('jio_forum_home_quick_post', true) && $index === (int) jio_get_setting('jio_forum_home_quick_post_tab', 2)) {
$html = QuickPost::render() . $html;
}
return $html;
}
public static function page(): int {
$raw = $_GET['fpage'] ?? 1;
return max(1, is_scalar($raw) ? absint($raw) : 1);
}
private static function posts_tab(array $tab, int $index, int $page): string {
$args = self::query_args($tab, $page);
$html = self::post_list($args, $tab, $index, $page, $tab['type'] === 'synthesis');
if ($html !== '') return $html;
$html = jio_get_ajax_null('内容空空如也');
if ($page === 1) {
$html .= '<div class="text-center mb20 ajax-item">' . self::create_plate_button('ml10 mr10 mt10 but hollow c-blue padding-lg') . self::publish_button('ml10 mr10 mt10 but hollow c-green padding-lg') . '</div>';
}
return $html;
}
private static function follow_tab(array $tab, int $index, int $page): string {
$user_id = get_current_user_id();
if (!$user_id) {
return '<div class="ajax-item">' . jio_get_null('登录后继续查看', 20, 'null-user.svg', '', 0, 220)
. '<p class="text-center mb20"><a class="but jb-blue padding-lg" href="' . esc_url(jio_get_login_url(Core::home_url())) . '"><i class="fa fas fa-sign-in-alt mr6"></i>登录</a></p></div>';
}
$followed = View::user_marked_ids($user_id, '_jio_forum_follow_');
$html = '';
if ($page === 1 && jio_get_setting('jio_forum_home_recommend_plate', true)) {
$plates = self::plates(['posts_per_page' => 8, 'post__not_in' => $followed]);
$slide = Cards::plate_slide($plates);
if ($slide !== '') {
$html .= '<div class="ajax-item mb20"><div class="box-body notop"><div class="title-theme">热门推荐</div></div>' . $slide . '</div>';
}
}
$list = '';
if ($followed) {
$args = self::query_args($tab, $page);
$args['meta_query'][] = ['key' => '_jio_forum_plate_id', 'value' => $followed, 'compare' => 'IN'];
$list = self::post_list($args, $tab, $index, $page);
}
return $html . ($list !== '' ? $list : jio_get_ajax_null('暂无内容', 60, 'null.svg'));
}
private static function pending_tab(array $tab, int $index, int $page): string {
$html = '';
if ($page === 1) {
foreach (get_posts(['post_type' => Core::PLATE, 'post_status' => 'pending', 'posts_per_page' => 20]) as $plate) {
$html .= '<div class="forum-pending-plate jio-widget-box flex ac jsb ajax-item"><span class="text-ellipsis"><span class="badg badg-sm c-yellow mr6">待审核版块</span>' . esc_html($plate->post_title) . '</span>'
. '<button type="button" class="but c-blue p2-10 px12 flex0" data-forum-action="plate-audit" data-id="' . (int) $plate->ID . '">通过审核</button></div>';
}
}
$list = self::post_list(['post_type' => Core::POST, 'post_status' => 'pending', 'posts_per_page' => self::per_page(), 'paged' => $page], $tab, $index, $page);
$html .= $list;
return $html !== '' ? $html : jio_get_ajax_null('暂无待审核内容');
}
private static function plate_tab(): string {
$html = '';
$user_id = get_current_user_id();
if ($user_id && jio_get_setting('jio_forum_home_follow_plate', true)) {
$followed = View::user_marked_ids($user_id, '_jio_forum_follow_');
$slide = $followed ? Cards::plate_slide(self::plates(['post__in' => $followed, 'posts_per_page' => 20]), 'mini', false, true) : '';
if ($slide !== '') $html .= self::panel('我关注的版块', $slide);
}
if (jio_get_setting('jio_forum_home_hot_plate', true)) {
$slide = Cards::plate_slide(self::plates(['posts_per_page' => 8, 'meta_key' => '_jio_forum_follow_count', 'orderby' => 'meta_value_num']));
if ($slide !== '') $html .= self::panel('热门推荐', $slide);
}
$all = self::plates(['posts_per_page' => -1, 'orderby' => 'menu_order title', 'order' => 'ASC']);
$groups = [];
$loose = [];
$terms = wp_get_object_terms(wp_list_pluck($all, 'ID'), Core::CATEGORY, ['fields' => 'all_with_object_id']);
$by_plate = [];
foreach (is_array($terms) ? $terms : [] as $term) {
$by_plate[(int) $term->object_id][] = $term;
}
foreach ($all as $plate) {
if (empty($by_plate[$plate->ID])) {
$loose[] = $plate;
continue;
}
foreach ($by_plate[$plate->ID] as $term) {
$groups[$term->term_id]['term'] = $term;
$groups[$term->term_id]['plates'][] = $plate;
}
}
uasort($groups, static fn(array $a, array $b): int => count($b['plates']) <=> count($a['plates']));
$plate_name = (string) jio_get_setting('jio_forum_plate_name', '版块') ?: '版块';
$single_group = count($groups) === 0 && $loose;
if ($loose) $groups[0] = ['term' => null, 'plates' => $loose];
$i = 1;
foreach ($groups as $term_id => $group) {
$name = $group['term'] instanceof \WP_Term ? $group['term']->name : ($single_group ? '全部' . $plate_name : '其他' . $plate_name);
$html .= self::plate_group('home-plate-panel-' . $i++, $name, $group['plates'], (int) $term_id);
}
if ($html === '') {
$html = jio_get_null('暂时没有版块', 60, 'null.svg', '', 0, 170) . '<div class="text-center mt20">' . self::create_plate_button('but hollow c-blue padding-lg') . '</div>';
}
return '<div class="ajax-item">' . $html . '</div>';
}
/** @param \WP_Post[] $plates */
private static function plate_group(string $id, string $name, array $plates, int $term_id): string {
$add = self::create_plate_button('plate-add ml20 but hollow c-blue p2-10 px12 flex0', $term_id);
$items = '';
foreach ($plates as $plate) {
$items .= Cards::plate_item($plate);
}
$title = '<span class="title-theme" title="' . esc_attr($name) . '"><span class="text-ellipsis">' . esc_html($name) . '</span></span>';
$top = wp_is_mobile()
? '<h4 class="panel-title flex ac">' . $title . $add . '</h4>'
: '<h4 class="panel-title flex ac"><a class="flex ac jsb flex1" data-toggle="collapse" href="#' . esc_attr($id) . '">' . $title . '<span><i class="ml10 mr10 fas fa-angle-up em14 muted-2-color"></i></span></a>' . $add . '</h4>';
return '<div class="panel panel-plate">' . $top . '<div id="' . esc_attr($id) . '" class="panel-collapse collapse in"><div class="plate-lists">' . $items . '</div></div></div>';
}
private static function panel(string $title, string $body): string {
return '<div class="panel panel-plate"><h4 class="panel-title" title="' . esc_attr($title) . '"><span class="title-theme">' . esc_html($title) . '</span></h4>' . $body . '</div>';
}
/** @return \WP_Post[] */
private static function plates(array $args): array {
$plates = get_posts(array_merge(['post_type' => Core::PLATE, 'post_status' => 'publish', 'orderby' => 'modified', 'order' => 'DESC', 'no_found_rows' => true], $args));
PlateStats::prime(wp_list_pluck($plates, 'ID'));
return $plates;
}
private static function query_args(array $tab, int $page): array {
$args = [
'post_type' => Core::POST,
'post_status' => 'publish',
'posts_per_page' => self::per_page(),
'paged' => $page,
'jio_forum_list' => $tab['orderby'],
'meta_query' => [],
];
if ($tab['filter'] === 'essence') {
$args['meta_query'][] = ['key' => '_jio_forum_essence', 'value' => 1];
} elseif (in_array($tab['filter'], ['question', 'vote'], true)) {
$args['meta_query'][] = ['key' => '_jio_forum_type', 'value' => $tab['filter']];
}
return $args;
}
private static function post_list(array $args, array $tab, int $index, int $page, bool $show_topping = false): string {
$query = new \WP_Query($args);
if (!$query->have_posts()) return '';
Cards::prime($query->posts);
$html = '';
foreach ($query->posts as $post) {
$html .= $tab['style'] === 'mini' || $tab['type'] === 'pending'
? Cards::post_mini($post, 'alone ajax-item', $show_topping)
: Cards::post_detail($post, 'alone ajax-item', true, $show_topping);
}
if ($page < (int) $query->max_num_pages) {
$next = add_query_arg(['index' => $index, 'fpage' => $page + 1], remove_query_arg(['index', 'fpage']));
$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', 2) . '</div>';
return $html;
}
private static function per_page(): int {
return max(1, min(50, (int) jio_get_setting('jio_forum_page_size', 20)));
}
public static function create_plate_button(string $class, int $term_id = 0): string {
$user_id = get_current_user_id();
if (!$user_id || (!Access::is_admin($user_id) && !jio_get_setting('jio_forum_user_plates', true))) return '';
$name = (string) jio_get_setting('jio_forum_plate_name', '版块') ?: '版块';
return '<button type="button" class="' . esc_attr($class) . '" data-forum-open="plate" data-category="' . (int) $term_id . '"><i class="fa fas fa-plus mr3"></i>创建' . esc_html($name) . '</button>';
}
public static function publish_button(string $class): string {
$name = (string) jio_get_setting('jio_forum_post_name', '帖子') ?: '帖子';
$url = is_user_logged_in() ? Core::edit_url() : jio_get_login_url(Core::edit_url());
return '<a class="' . esc_attr($class) . '" href="' . esc_url($url) . '"><i class="fa fas fa-edit mr3"></i>发布' . esc_html($name) . '</a>';
}
}
+98
View File
@@ -0,0 +1,98 @@
<?php
namespace Jio\Forum;
defined('ABSPATH') || exit;
final class HomeTabs {
public const TYPES = ['follow', 'synthesis', 'plate', 'posts'];
public const ORDERS = ['date', 'modified', 'last_reply', 'popular', 'views', 'score'];
public const FILTERS = ['', 'essence', 'question', 'vote'];
/** @return array<int, array<string, mixed>> */
public static function defaults(): array {
$row = static fn(string $type, string $title, string $orderby = 'date', string $filter = ''): array => [
'title' => $title, 'type' => $type, 'orderby' => $orderby, 'filter' => $filter, 'style' => 'detail', 'show' => ['pc_s', 'm_s'],
];
return [
$row('follow', '关注'),
$row('synthesis', '综合'),
$row('plate', '版块'),
$row('posts', '热门', 'views'),
$row('posts', '精华', 'modified', 'essence'),
$row('posts', '问答', 'modified', 'question'),
$row('posts', '投票', 'modified', 'vote'),
$row('posts', '最新回复', 'last_reply'),
$row('posts', '最高评分', 'score'),
];
}
/** Tabs visible on the current device, numbered from 1 like the `index` query argument. */
public static function visible(): array {
$rows = jio_get_setting('jio_forum_home_tab_items', self::defaults());
$device = wp_is_mobile() ? 'm_s' : 'pc_s';
$tabs = [];
foreach (is_array($rows) ? $rows : [] as $row) {
if (!is_array($row) || !in_array($device, (array) ($row['show'] ?? []), true)) continue;
$type = in_array($row['type'] ?? '', self::TYPES, true) ? $row['type'] : 'posts';
$tabs[] = [
'title' => sanitize_text_field((string) ($row['title'] ?? '')) ?: '栏目',
'type' => $type,
'orderby' => in_array($row['orderby'] ?? '', self::ORDERS, true) ? $row['orderby'] : 'date',
'filter' => in_array($row['filter'] ?? '', self::FILTERS, true) ? $row['filter'] : '',
'style' => ($row['style'] ?? '') === 'mini' ? 'mini' : 'detail',
];
}
if (Access::is_admin(get_current_user_id())) {
$tabs[] = ['title' => '待审核', 'type' => 'pending', 'orderby' => 'date', 'filter' => '', 'style' => 'mini'];
}
return $tabs ? array_combine(range(1, count($tabs)), $tabs) : [];
}
public static function active_index(int $count): int {
$raw = $_GET['index'] ?? 0;
$index = (is_scalar($raw) ? absint($raw) : 0) ?: (int) jio_get_setting('jio_forum_home_tab_active', 2);
return $index >= 1 && $index <= $count ? $index : 1;
}
public static function render(): void {
$tabs = self::visible();
if (!$tabs) return;
$active = self::active_index(count($tabs));
$base = remove_query_arg(['index', 'fpage']);
$nav = $panes = '';
foreach ($tabs as $index => $tab) {
$id = 'home-tab-' . $index;
$href = add_query_arg('index', $index, $base);
$label = esc_html($tab['title']);
$nav .= '<li' . ($index === $active ? ' class="active"' : '') . '><a href="' . esc_url($href) . '" data-forum-tab="#' . $id . '">' . $label . '</a></li>';
$panes .= '<div class="forum-tab-pane tab-pane fade' . ($index === $active ? ' in active' : '') . '" id="' . $id . '" data-href="' . esc_url($href) . '">';
if ($index === $active) {
$panes .= HomeContent::render($tab, $index);
} else {
$panes .= '<div class="post_ajax_loader">' . self::placeholder($tab['type'] === 'plate' ? 'plate' : 'posts', $tab['type'] === 'plate' ? 2 : 4) . '</div>';
}
$panes .= '</div>';
}
echo '<div class="fixed-wrap forum-home-tab">';
echo '<div class="fixed-wrap-nav jio-panel affix-header-sm" offset-top="-8"><ul class="list-inline scroll-x mini-scrollbar tab-nav-theme">' . $nav . '</ul></div>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo '<div class="fixed-wrap-content">';
\Jio\WidgetSystem\WidgetAreas::render('forum_home_top_content');
echo '<div class="tab-content forum-main-tab-content">' . $panes . '</div>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
\Jio\WidgetSystem\WidgetAreas::render('forum_home_bottom_content');
echo '</div></div>';
}
public static function placeholder(string $type, int $count): string {
if ($type === 'plate') {
$card = '<div class="plate-card" style="margin-right: 10px;"><div class="plate-thumb"><div style="width: 100%; height: 100%;" class="placeholder radius"></div></div><div style="height: 30px;" class="placeholder k2 mt10"></div><div class="placeholder s1 mt20"></div></div>';
$item = '<div class="plate-item"><div class="plate-thumb"><div style="height: 100%;" class="placeholder radius"></div></div><div class="item-info"><div class="placeholder k1"></div><div style="height: 30px;" class="placeholder k2 hide-sm mt10"></div><div class="placeholder s1 mt10"></div></div></div>';
$block = '<div class="panel-plate mb20"><i class="placeholder s1" style="height: 20px;"></i><div class="flex scroll-plate">' . str_repeat($card, 4) . '</div></div>'
. '<div class="panel-plate mb20"><i style="height: 20px;" class="placeholder s1"></i><div class="plate-lists">' . str_repeat($item, 4) . '</div></div>';
return str_repeat($block, max(1, $count));
}
$post = '<div class="forum-posts detail alone"><div class="user-info flex ac mb6"><div class="avatar-img"><div style="width: 100%;height: 100%;" class="placeholder radius"></div></div><div class="flex1 ml10"><div class="placeholder k1" style="height: 15px;"></div><div class="placeholder s1 mt6"></div></div></div><h2 style="height: 40px;margin-top: 0;" class="forum-title placeholder k2"></h2><div class="flex ac jsb mt10"><i><i class="placeholder s1"></i><i class="placeholder s1 ml10"></i></i><i><i class="placeholder s1 ml10"></i></i></div></div>';
return str_repeat($post, max(1, $count));
}
}
+67
View File
@@ -0,0 +1,67 @@
<?php
namespace Jio\Forum;
defined('ABSPATH') || exit;
final class HomeView {
/** Forum home: fluid widgets, tab content column and sidebar. */
public static function render(): void {
if (!Core::enabled()) {
status_header(404);
include get_404_template();
return;
}
get_header();
echo '<main id="forum" class="jio-forum forum-home">';
\Jio\WidgetSystem\WidgetAreas::render('forum_home_top_fluid', 'fluid-widget-wrap');
echo '<div class="container"><div class="content-wrap"><div class="content-layout">';
echo '<div class="forum-home__main">';
HomeTabs::render();
echo '</div></div></div>';
self::sidebar();
echo '</div>';
\Jio\WidgetSystem\WidgetAreas::render('forum_home_bottom_fluid', 'fluid-widget-wrap');
echo '</main>';
get_footer();
}
private static function sidebar(): void {
$plate_name = (string) jio_get_setting('jio_forum_plate_name', '版块') ?: '版块';
echo '<div class="sidebar forum-home__sidebar">';
if (is_user_logged_in()) {
echo '<div class="jio-widget-box"><a class="but jb-blue btn-block" href="' . esc_url(Core::edit_url()) . '"><i class="fa fas fa-edit mr6"></i>发布' . esc_html((string) jio_get_setting('jio_forum_post_name', '帖子')) . '</a>';
if (jio_get_setting('jio_forum_user_plates', true)) {
echo '<button type="button" class="but hollow c-blue btn-block mt10" data-forum-open="plate">创建' . esc_html($plate_name) . '</button>';
self::plate_dialog();
}
echo '</div>';
} else {
echo '<div class="jio-widget-box"><a class="but jb-blue btn-block" href="' . esc_url(jio_get_login_url(Core::edit_url())) . '"><i class="fa fas fa-sign-in-alt mr6"></i>登录后发帖</a></div>';
}
\Jio\WidgetSystem\WidgetAreas::render('forum_home_sidebar');
$plates = get_posts(['post_type' => Core::PLATE, 'post_status' => 'publish', 'posts_per_page' => 8, 'orderby' => 'modified']);
if ($plates) {
PlateStats::prime(wp_list_pluck($plates, 'ID'));
echo '<div class="jio-widget-box"><h4 class="title-theme mb10">热门' . esc_html($plate_name) . '</h4>';
foreach ($plates as $plate) {
$stats = PlateStats::get($plate->ID);
echo '<a class="jio-forum__widget-link" href="' . esc_url(get_permalink($plate)) . '"><span class="text-ellipsis">' . esc_html($plate->post_title) . '</span><span>' . esc_html(jio_cut_count($stats['posts'])) . '</span></a>';
}
echo '</div>';
}
echo '</div>';
}
private static function plate_dialog(): void {
echo '<dialog class="jio-forum__dialog" data-forum-dialog="plate"><form data-forum-form="plate"><h2>创建版块</h2>'
. '<label>名称<input name="title" required maxlength="80"></label>'
. '<label>介绍<textarea name="content" rows="5"></textarea></label>'
. '<label>分类<select name="category_id"><option value="0">未分类</option>';
$terms = get_terms(['taxonomy' => Core::CATEGORY, 'hide_empty' => false]);
foreach (is_array($terms) ? $terms : [] as $term) {
echo '<option value="' . (int) $term->term_id . '">' . esc_html($term->name) . '</option>';
}
echo '</select></label><p class="jio-forum__message" role="status"></p><div class="jio-forum__dialog-actions"><button type="button" data-forum-close>取消</button><button type="submit" class="jio-forum__primary">提交</button></div></form></dialog>';
}
}
+11 -1
View File
@@ -60,7 +60,7 @@ final class Integration {
public static function sort_forum_posts(array $clauses, \WP_Query $query): array { public static function sort_forum_posts(array $clauses, \WP_Query $query): array {
$mode = sanitize_key((string) $query->get('jio_forum_list')); $mode = sanitize_key((string) $query->get('jio_forum_list'));
if (!in_array($mode, ['latest', 'popular', 'question', 'vote', 'essence', 'follow'], true)) return $clauses; if (!in_array($mode, ['latest', 'date', 'modified', 'views', 'score', 'popular', 'last_reply', 'question', 'vote', 'essence', 'follow'], true)) return $clauses;
global $wpdb; global $wpdb;
$clauses['join'] .= " LEFT JOIN {$wpdb->postmeta} AS jio_forum_pin ON ({$wpdb->posts}.ID = jio_forum_pin.post_id AND jio_forum_pin.meta_key = '_jio_forum_pinned')"; $clauses['join'] .= " LEFT JOIN {$wpdb->postmeta} AS jio_forum_pin ON ({$wpdb->posts}.ID = jio_forum_pin.post_id AND jio_forum_pin.meta_key = '_jio_forum_pinned')";
$pin = 'CAST(COALESCE(jio_forum_pin.meta_value, 0) AS SIGNED) DESC'; $pin = 'CAST(COALESCE(jio_forum_pin.meta_value, 0) AS SIGNED) DESC';
@@ -71,6 +71,16 @@ final class Integration {
$clauses['join'] .= " LEFT JOIN {$wpdb->postmeta} AS jio_forum_views ON ({$wpdb->posts}.ID = jio_forum_views.post_id AND jio_forum_views.meta_key = '_jio_forum_views')"; $clauses['join'] .= " LEFT JOIN {$wpdb->postmeta} AS jio_forum_views ON ({$wpdb->posts}.ID = jio_forum_views.post_id AND jio_forum_views.meta_key = '_jio_forum_views')";
$rank = "({$wpdb->posts}.comment_count * 2 + CAST(COALESCE(jio_forum_up.meta_value, 0) AS SIGNED) * 10 - CAST(COALESCE(jio_forum_down.meta_value, 0) AS SIGNED) * 15 + CAST(COALESCE(jio_forum_fav.meta_value, 0) AS SIGNED) * 10 + CAST(COALESCE(jio_forum_views.meta_value, 0) AS SIGNED))"; $rank = "({$wpdb->posts}.comment_count * 2 + CAST(COALESCE(jio_forum_up.meta_value, 0) AS SIGNED) * 10 - CAST(COALESCE(jio_forum_down.meta_value, 0) AS SIGNED) * 15 + CAST(COALESCE(jio_forum_fav.meta_value, 0) AS SIGNED) * 10 + CAST(COALESCE(jio_forum_views.meta_value, 0) AS SIGNED))";
$clauses['orderby'] = $pin . ', ' . $rank . ' DESC, ' . $wpdb->posts . '.post_date DESC'; $clauses['orderby'] = $pin . ', ' . $rank . ' DESC, ' . $wpdb->posts . '.post_date DESC';
} elseif ($mode === 'views' || $mode === 'score') {
$key = $mode === 'views' ? '_jio_forum_views' : '_jio_forum_score';
$alias = 'jio_forum_sort_' . $mode;
$clauses['join'] .= " LEFT JOIN {$wpdb->postmeta} AS $alias ON ({$wpdb->posts}.ID = $alias.post_id AND $alias.meta_key = '$key')";
$clauses['orderby'] = $pin . ', CAST(COALESCE(' . $alias . '.meta_value, 0) AS SIGNED) DESC, ' . $wpdb->posts . '.post_date DESC';
} elseif ($mode === 'modified') {
$clauses['orderby'] = $pin . ', ' . $wpdb->posts . '.post_modified DESC';
} elseif ($mode === 'last_reply') {
$clauses['join'] .= " LEFT JOIN (SELECT comment_post_ID AS jio_reply_post_id, MAX(comment_date) AS jio_reply_last FROM {$wpdb->comments} WHERE comment_approved = '1' GROUP BY comment_post_ID) AS jio_forum_last ON ({$wpdb->posts}.ID = jio_forum_last.jio_reply_post_id)";
$clauses['orderby'] = $pin . ', COALESCE(jio_forum_last.jio_reply_last, ' . $wpdb->posts . '.post_date) DESC';
} else { } else {
$clauses['orderby'] = $pin . ', ' . $wpdb->posts . '.post_date DESC'; $clauses['orderby'] = $pin . ', ' . $wpdb->posts . '.post_date DESC';
} }
+37
View File
@@ -0,0 +1,37 @@
<?php
namespace Jio\Forum;
defined('ABSPATH') || exit;
final class PlateStats {
/** @var array<int, array{posts:int, today:int}> */
private static array $stats = [];
/** Load post totals and today's posts for many plates with one grouped query. */
public static function prime(array $plate_ids): void {
$ids = array_values(array_diff(array_unique(array_filter(array_map('absint', $plate_ids))), array_keys(self::$stats)));
if (!$ids) return;
foreach ($ids as $id) {
self::$stats[$id] = ['posts' => 0, 'today' => 0];
}
global $wpdb;
$placeholders = implode(',', array_fill(0, count($ids), '%d'));
$today = wp_date('Y-m-d 00:00:00');
$sql = "SELECT CAST(pm.meta_value AS UNSIGNED) AS plate_id, COUNT(*) AS total, SUM(p.post_date >= %s) AS today
FROM {$wpdb->posts} p
INNER JOIN {$wpdb->postmeta} pm ON pm.post_id = p.ID AND pm.meta_key = '_jio_forum_plate_id'
WHERE p.post_type = %s AND p.post_status = 'publish' AND CAST(pm.meta_value AS UNSIGNED) IN ($placeholders)
GROUP BY plate_id";
$rows = $wpdb->get_results($wpdb->prepare($sql, array_merge([$today, Core::POST], $ids))); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
foreach ((array) $rows as $row) {
self::$stats[(int) $row->plate_id] = ['posts' => (int) $row->total, 'today' => (int) $row->today];
}
}
/** @return array{posts:int, today:int} */
public static function get(int $plate_id): array {
if (!isset(self::$stats[$plate_id])) self::prime([$plate_id]);
return self::$stats[$plate_id] ?? ['posts' => 0, 'today' => 0];
}
}
+38
View File
@@ -0,0 +1,38 @@
<?php
namespace Jio\Forum;
defined('ABSPATH') || exit;
final class QuickPost {
/** Inline "publish now" box shown on the configured home tab. */
public static function render(): string {
$user_id = get_current_user_id();
if (!$user_id) {
return '<div class="ajax-item-header jio-widget-box quick-posts-box mb20"><p class="muted-2-color">登录后即可快速发布' . esc_html(self::post_name()) . '</p><a class="but jb-blue padding-lg" href="' . esc_url(jio_get_login_url(Core::home_url())) . '"><i class="fa fas fa-sign-in-alt mr6"></i>登录</a></div>';
}
$plates = get_posts(['post_type' => Core::PLATE, 'post_status' => 'publish', 'posts_per_page' => 100, 'orderby' => 'title', 'order' => 'ASC']);
$options = '';
foreach ($plates as $plate) {
$options .= '<option value="' . (int) $plate->ID . '">' . esc_html($plate->post_title) . '</option>';
}
if ($options === '') return '';
return '<div class="ajax-item-header jio-widget-box quick-posts-box mb20">'
. '<form data-forum-form="post" class="quick-posts-form"><h4 class="title-theme mb10">快速发布' . esc_html(self::post_name()) . '</h4>'
. '<input class="form-control mb10" type="text" name="title" required maxlength="120" placeholder="输入标题" aria-label="标题">'
. '<textarea class="form-control mb10" name="content" rows="3" required placeholder="说点什么…" aria-label="正文"></textarea>'
. '<div class="flex ac jsb">'
. '<select class="form-control w-auto" name="plate_id" required aria-label="' . esc_attr(self::plate_name()) . '"><option value="">选择' . esc_html(self::plate_name()) . '</option>' . $options . '</select>'
. '<button type="submit" class="but jb-blue"><i class="fa fas fa-paper-plane mr6"></i>发布</button>'
. '</div><p class="jio-forum__message mt6 muted-2-color" role="status"></p></form></div>';
}
private static function post_name(): string {
return (string) jio_get_setting('jio_forum_post_name', '帖子') ?: '帖子';
}
private static function plate_name(): string {
return (string) jio_get_setting('jio_forum_plate_name', '版块') ?: '版块';
}
}
+33 -121
View File
@@ -24,10 +24,6 @@ final class View {
echo '<aside class="jio-forum__aside">'; echo '<aside class="jio-forum__aside">';
if (is_user_logged_in()) { if (is_user_logged_in()) {
echo '<a class="jio-forum__new" href="' . esc_url(Core::edit_url()) . '">发布帖子</a>'; echo '<a class="jio-forum__new" href="' . esc_url(Core::edit_url()) . '">发布帖子</a>';
if ($context === 'home' && jio_get_setting('jio_forum_user_plates', true)) {
echo '<button type="button" class="jio-forum__action jio-forum__create-plate" data-forum-open="plate">创建版块</button>';
echo '<dialog class="jio-forum__dialog" data-forum-dialog="plate"><form data-forum-form="plate"><h2>创建版块</h2><label>名称<input name="title" required maxlength="80"></label><label>介绍<textarea name="content" rows="5"></textarea></label><p class="jio-forum__message" role="status"></p><div class="jio-forum__dialog-actions"><button type="button" data-forum-close>取消</button><button type="submit" class="jio-forum__primary">提交</button></div></form></dialog>';
}
} else { } else {
echo '<a class="jio-forum__new" href="' . esc_url(jio_get_login_url(Core::edit_url())) . '">登录后发帖</a>'; echo '<a class="jio-forum__new" href="' . esc_url(jio_get_login_url(Core::edit_url())) . '">登录后发帖</a>';
} }
@@ -44,37 +40,7 @@ final class View {
} }
public static function home(): void { public static function home(): void {
self::open('论坛', 'home'); HomeView::render();
\Jio\WidgetSystem\WidgetAreas::render('forum_home_top_fluid');
echo '<div class="jio-forum__layout"><div class="jio-forum__main">';
$name = (string) jio_get_setting('jio_forum_forum_name', '论坛');
echo '<header class="jio-forum__hero"><h1>' . esc_html($name) . '</h1></header>';
if (Access::is_admin(get_current_user_id())) {
$pending = get_posts(['post_type' => Core::PLATE, 'post_status' => 'pending', 'posts_per_page' => 20]);
if ($pending) {
echo '<section class="jio-forum__pending"><h2>待审核版块</h2>';
foreach ($pending as $plate) {
echo '<div><span>' . esc_html($plate->post_title) . '</span><button type="button" data-forum-action="plate-audit" data-id="' . (int) $plate->ID . '">通过审核</button></div>';
}
echo '</section>';
}
self::term_management();
}
self::tabs();
$categories = get_terms(['taxonomy' => Core::CATEGORY, 'hide_empty' => false]);
if (is_array($categories) && $categories) {
echo '<div class="jio-forum__chips">';
foreach ($categories as $category) {
echo '<a href="' . esc_url(get_term_link($category)) . '">' . esc_html($category->name) . '</a>';
}
echo '</div>';
}
self::posts(['post_type' => Core::POST, 'post_status' => 'publish'], 'home');
echo '</div>';
self::sidebar('home');
echo '</div>';
\Jio\WidgetSystem\WidgetAreas::render('forum_home_bottom_fluid');
self::close();
} }
public static function listing(): void { public static function listing(): void {
@@ -90,7 +56,7 @@ final class View {
if ($plate) { if ($plate) {
echo '<div class="jio-forum__description">' . wp_kses_post(wpautop($plate->post_excerpt ?: $plate->post_content)) . '</div>'; 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 '<p class="jio-forum__stats">' . (int) get_post_meta($plate->ID, '_jio_forum_follow_count', true) . ' 人关注</p>';
self::action('follow', $plate->ID, Access::follows(get_current_user_id(), $plate->ID) ? '已关注' : '关注版块'); 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)) { if (Access::is_moderator(get_current_user_id(), $plate->ID)) {
self::plate_editor($plate); self::plate_editor($plate);
} }
@@ -128,14 +94,12 @@ final class View {
self::close(); self::close();
} }
/** Listing pages (plate / taxonomy) use a fixed, always-available tab set. */
public static function tabs(): void { public static function tabs(): void {
$tabs = ['latest' => '最新', 'popular' => '热门', 'essence' => '精华', 'question' => '问答', 'vote' => '投票', 'plates' => '版块', 'follow' => '已关注']; $tabs = ['latest' => '最新', 'popular' => '热门', 'essence' => '精华', 'question' => '问答', 'vote' => '投票'];
$enabled = (array) jio_get_setting('jio_forum_home_tabs', array_keys($tabs));
$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()); $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="论坛栏目">'; echo '<nav class="jio-forum__tabs" aria-label="论坛栏目">';
foreach ($tabs as $key => $label) { foreach ($tabs as $key => $label) {
if (!in_array($key, $enabled, true)) continue;
if ($key === 'follow' && !is_user_logged_in()) continue;
$url = add_query_arg('forum_view', $key, $base_url); $url = add_query_arg('forum_view', $key, $base_url);
$selected = (string) ($_GET['forum_view'] ?? 'latest') === $key; $selected = (string) ($_GET['forum_view'] ?? 'latest') === $key;
echo '<a class="' . ($selected ? 'is-active' : '') . '" href="' . esc_url($url) . '">' . esc_html($label) . '</a>'; echo '<a class="' . ($selected ? 'is-active' : '') . '" href="' . esc_url($url) . '">' . esc_html($label) . '</a>';
@@ -145,13 +109,8 @@ final class View {
public static function posts(array $args, string $context = 'home'): void { public static function posts(array $args, string $context = 'home'): void {
$view = sanitize_key((string) ($_GET['forum_view'] ?? 'latest')); $view = sanitize_key((string) ($_GET['forum_view'] ?? 'latest'));
if ($view === 'plates') { if (!in_array($view, ['latest', 'popular', 'essence', 'question', 'vote'], true)) {
self::plates([]); $view = 'latest';
return;
}
if ($view === 'follow') {
$ids = self::user_marked_ids(get_current_user_id(), '_jio_forum_follow_');
$args['meta_query'][] = ['key' => '_jio_forum_plate_id', 'value' => $ids ?: [0], 'compare' => 'IN'];
} }
if (in_array($view, ['question', 'vote'], true)) { if (in_array($view, ['question', 'vote'], true)) {
$args['meta_query'][] = ['key' => '_jio_forum_type', 'value' => $view]; $args['meta_query'][] = ['key' => '_jio_forum_type', 'value' => $view];
@@ -163,77 +122,43 @@ final class View {
$args['paged'] = max(1, (int) get_query_var('paged', 1)); $args['paged'] = max(1, (int) get_query_var('paged', 1));
$args['orderby'] = 'date'; $args['orderby'] = 'date';
$args['order'] = 'DESC'; $args['order'] = 'DESC';
$args['jio_forum_list'] = in_array($view, ['latest', 'popular', 'question', 'vote', 'essence', 'follow'], true) ? $view : 'latest'; $args['jio_forum_list'] = $view;
$query = new \WP_Query($args); $query = new \WP_Query($args);
\Jio\WidgetSystem\WidgetAreas::render('forum_' . $context . '_top_content'); \Jio\WidgetSystem\WidgetAreas::render('forum_' . $context . '_top_content');
echo '<div class="jio-forum__list">';
if (!$query->have_posts()) { if (!$query->have_posts()) {
echo '<p class="jio-forum__empty">暂无帖子</p>'; echo '<div class="jio-forum__list">' . jio_get_null('暂无帖子') . '</div>';
} else {
Cards::prime($query->posts);
echo '<div class="jio-forum__list">';
foreach ($query->posts as $post) {
echo Cards::post_detail($post, 'ajax-item'); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
echo '</div>';
} }
while ($query->have_posts()) {
$query->the_post();
self::post_card(get_post());
}
wp_reset_postdata();
echo '</div>';
self::pagination($query); self::pagination($query);
\Jio\WidgetSystem\WidgetAreas::render('forum_' . $context . '_bottom_content'); \Jio\WidgetSystem\WidgetAreas::render('forum_' . $context . '_bottom_content');
} }
public static function plates(array $args): void { public static function plates(array $args): void {
$query = new \WP_Query(array_merge(['post_type' => Core::PLATE, 'post_status' => 'publish', 'posts_per_page' => 20], $args)); $query = new \WP_Query(array_merge(['post_type' => Core::PLATE, 'post_status' => 'publish', 'posts_per_page' => 20], $args));
echo '<div class="jio-forum__plates">'; if (!$query->have_posts()) {
while ($query->have_posts()) { echo jio_get_null('暂无版块'); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
$query->the_post(); return;
$plate = get_post(); }
self::plate_card($plate); PlateStats::prime($query->posts ? wp_list_pluck($query->posts, 'ID') : []);
echo '<div class="plate-lists">';
foreach ($query->posts as $plate) {
echo Cards::plate_item($plate); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
} }
wp_reset_postdata();
echo '</div>'; echo '</div>';
} }
public static function plate_card(\WP_Post $plate): void { public static function plate_card(\WP_Post $plate): void {
$url = get_permalink($plate); echo Cards::plate_item($plate); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo '<article class="jio-forum__plate"><a class="jio-forum__plate-cover" href="' . esc_url($url) . '">';
if (jio_get_setting('jio_forum_show_plate_cover', true) && has_post_thumbnail($plate)) {
echo get_the_post_thumbnail($plate, 'jio-card');
}
echo '</a><div><h2><a href="' . esc_url($url) . '">' . esc_html($plate->post_title) . '</a></h2><p>' . esc_html(wp_trim_words(wp_strip_all_tags($plate->post_excerpt ?: $plate->post_content), 22)) . '</p><small>' . (int) get_post_meta($plate->ID, '_jio_forum_follow_count', true) . ' 人关注</small></div></article>';
} }
public static function post_card(\WP_Post $post): void { public static function post_card(\WP_Post $post): void {
$plate_id = (int) get_post_meta($post->ID, '_jio_forum_plate_id', true); echo Cards::post_detail($post); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
$type = (string) get_post_meta($post->ID, '_jio_forum_type', true);
echo '<article class="jio-forum__post"><a class="jio-forum__avatar" href="' . esc_url(get_author_posts_url((int) $post->post_author)) . '">' . get_avatar((int) $post->post_author, 44) . '</a><div class="jio-forum__post-body">';
echo '<div class="jio-forum__post-top"><span>' . esc_html(get_the_author_meta('display_name', (int) $post->post_author)) . '</span><time datetime="' . esc_attr(get_the_date(DATE_W3C, $post)) . '">' . esc_html(get_the_date('', $post)) . '</time></div>';
$url = $post->post_status === 'publish' ? get_permalink($post) : Core::edit_url($post->ID);
echo '<h2><a href="' . esc_url($url) . '">' . esc_html($post->post_title) . '</a></h2>';
$excerpt = Access::can_view(get_current_user_id(), $post->ID) ? wp_trim_words(wp_strip_all_tags($post->post_excerpt ?: $post->post_content), 30) : '内容已隐藏';
echo '<p>' . esc_html($excerpt) . '</p>';
echo '<div class="jio-forum__post-meta">';
if (get_post_meta($post->ID, '_jio_forum_pinned', true)) {
echo '<span class="jio-forum__badge">置顶</span>';
}
if ($type !== '') {
echo '<span class="jio-forum__badge">' . esc_html($type === 'question' ? '问答' : '投票') . '</span>';
}
if ($type === 'question' && get_post_meta($post->ID, '_jio_forum_adopted_comment', true)) {
echo '<span class="jio-forum__badge">已解决</span>';
}
if (get_post_meta($post->ID, '_jio_forum_essence', true)) {
echo '<span class="jio-forum__badge">精华</span>';
}
if ($plate_id > 0) {
echo '<a href="' . esc_url(get_permalink($plate_id)) . '">' . esc_html(get_the_title($plate_id)) . '</a>';
}
echo '<span>' . (int) get_post_meta($post->ID, '_jio_forum_views', true) . ' 阅读</span>';
echo '<span>' . (int) $post->comment_count . ' 回复</span>';
echo '<span>' . (int) get_post_meta($post->ID, '_jio_forum_score', true) . ' 评分</span>';
echo '<span>' . (int) get_post_meta($post->ID, '_jio_forum_favorite_count', true) . ' 收藏</span></div></div></article>';
$hot = get_comments(['post_id' => $post->ID, 'status' => 'approve', 'number' => 1, 'meta_key' => '_jio_forum_hot', 'meta_value' => '1']);
if ($hot) {
echo '<div class="jio-forum__hot-reply">热门回复:' . esc_html(wp_trim_words(wp_strip_all_tags($hot[0]->comment_content), 25)) . '</div>';
}
} }
public static function replies(int $post_id): void { public static function replies(int $post_id): void {
@@ -278,18 +203,6 @@ final class View {
echo '</section>'; echo '</section>';
} }
private static function term_management(): void {
echo '<section class="jio-forum__term-management"><h2>分类与话题</h2><form data-forum-form="term"><label>类型<select name="type"><option value="category">版块分类</option><option value="topic">论坛话题</option><option value="tag">论坛标签</option></select></label><label>名称<input name="name" required maxlength="60"></label><p class="jio-forum__message" role="status"></p><button type="submit" class="jio-forum__action">添加</button></form>';
foreach (['category' => Core::CATEGORY, 'topic' => Core::TOPIC, 'tag' => Core::TAG] as $label => $taxonomy) {
$terms = get_terms(['taxonomy' => $taxonomy, 'hide_empty' => false, 'number' => 30]);
if (!is_array($terms)) continue;
foreach ($terms as $term) {
echo '<div class="jio-forum__term-row"><span>' . esc_html($term->name) . '</span><button type="button" data-forum-action="term-edit" data-id="' . (int) $term->term_id . '">编辑</button><button type="button" data-forum-action="term-delete" data-id="' . (int) $term->term_id . '">删除</button></div>';
}
}
echo '</section>';
}
private static function plate_editor(\WP_Post $plate): void { private static function plate_editor(\WP_Post $plate): void {
$id = $plate->ID; $id = $plate->ID;
echo '<button type="button" class="jio-forum__action" data-forum-open="plate-edit-' . $id . '">编辑版块</button>'; echo '<button type="button" class="jio-forum__action" data-forum-open="plate-edit-' . $id . '">编辑版块</button>';
@@ -371,17 +284,16 @@ final class View {
$query = new \WP_Query($args); $query = new \WP_Query($args);
echo '<div class="jio-forum__list">'; echo '<div class="jio-forum__list">';
if (!$query->have_posts()) echo '<p class="jio-forum__empty">暂无内容</p>'; if (!$query->have_posts()) echo '<p class="jio-forum__empty">暂无内容</p>';
while ($query->have_posts()) { Cards::prime($query->posts);
$query->the_post(); foreach ($query->posts as $post) {
$post = get_post(); if ($post->post_type === Core::POST) {
if ($post->post_type === Core::POST) self::post_card($post); echo Cards::post_detail($post); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
else { continue;
$url = $post->post_status === 'publish' ? get_permalink($post) : '';
echo '<div class="jio-forum__user-row">' . ($url ? '<a href="' . esc_url($url) . '">' : '<span>') . esc_html($post->post_title) . ($url ? '</a>' : '</span>') . ($url ? '' : '<small>待审核</small>') . '</div>';
if ($owner && !$url) self::plate_editor($post);
} }
$url = $post->post_status === 'publish' ? get_permalink($post) : '';
echo '<div class="jio-forum__user-row">' . ($url ? '<a href="' . esc_url($url) . '">' : '<span>') . esc_html($post->post_title) . ($url ? '</a>' : '</span>') . ($url ? '' : '<small>待审核</small>') . '</div>';
if ($owner && !$url) self::plate_editor($post);
} }
wp_reset_postdata();
echo '</div></div>'; echo '</div></div>';
} }
} }
+1 -1
View File
@@ -69,7 +69,7 @@ async function run() {
}) })
await sleep(800) await sleep(800)
const result = await call('Runtime.evaluate', { const result = await call('Runtime.evaluate', {
expression: `({forum:!!document.getElementById('forum'),posts:document.querySelectorAll('.jio-forum__post').length,locked:document.body.textContent.includes('内容已隐藏'),leaked:document.documentElement.innerHTML.includes('需要积分查看的正文'),overflow:document.documentElement.scrollWidth>document.documentElement.clientWidth,width:innerWidth})`, expression: `({forum:!!document.getElementById('forum'),posts:document.querySelectorAll('.forum-posts').length,locked:document.body.textContent.includes('内容已隐藏'),leaked:document.documentElement.innerHTML.includes('需要积分查看的正文'),overflow:document.documentElement.scrollWidth>document.documentElement.clientWidth,width:innerWidth})`,
returnByValue: true, returnByValue: true,
}) })
const state = result.result.value const state = result.result.value
+2 -2
View File
@@ -23,12 +23,12 @@ test('every forum page mounts the dedicated runtime and the public write routes
test('paid content has server-side presentation guards beyond its page body', () => { test('paid content has server-side presentation guards beyond its page body', () => {
const access = read('src/Forum/Access.php') const access = read('src/Forum/Access.php')
const integration = read('src/Forum/Integration.php') const integration = read('src/Forum/Integration.php')
const view = read('src/Forum/View.php') const cards = read('src/Forum/Cards.php')
assert.match(access, /function can_view\(/) assert.match(access, /function can_view\(/)
assert.match(integration, /add_filter\('the_content'/) assert.match(integration, /add_filter\('the_content'/)
assert.match(integration, /add_filter\('get_the_excerpt'/) assert.match(integration, /add_filter\('get_the_excerpt'/)
assert.match(integration, /add_filter\('jio_seo_head_payload'/) assert.match(integration, /add_filter\('jio_seo_head_payload'/)
assert.match(view, /Access::can_view\(get_current_user_id\(\), \$post->ID\)/) assert.match(cards, /Access::can_view\(get_current_user_id\(\), \$id\)/)
}) })
test('forum management, account, search and responsive styles remain connected', () => { test('forum management, account, search and responsive styles remain connected', () => {