商品以及商品详情

This commit is contained in:
liu
2026-07-10 20:14:43 +08:00
parent c25cf0e8b3
commit 3022990ea1
93 changed files with 4576 additions and 1008 deletions
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
.profile-page{background:#f7f8fa;min-height:100vh}.profile-header{background:linear-gradient(160deg,#1989fa,#07c160);overflow:hidden;padding-bottom:.8rem;position:relative}.profile-header::before{background:hsla(0,0%,100%,.08);border-radius:50%;content:"";height:7rem;position:absolute;right:-1.5rem;top:-2rem;width:7rem}.profile-header::after{background:hsla(0,0%,100%,.05);border-radius:50%;bottom:-1rem;content:"";height:5rem;left:-1rem;position:absolute;width:5rem}.header-actions{display:flex;justify-content:flex-end;padding:.3rem .6rem 0}.header-actions .settings-btn{align-items:center;-webkit-backdrop-filter:blur(.25rem);backdrop-filter:blur(.25rem);background:hsla(0,0%,100%,.15);border-radius:50%;display:flex;height:1.5rem;justify-content:center;width:1.5rem}.user-card{align-items:center;display:flex;padding:.6rem .8rem .5rem;position:relative;z-index:1}.user-card .avatar{border:.1rem solid hsla(0,0%,100%,.4);border-radius:50%;flex-shrink:0;height:3rem;width:3rem}.user-card .user-info{flex:1;margin-left:.6rem}.user-card .user-info .name-row{align-items:center;display:flex;margin-bottom:.2rem}.user-card .user-info .name-row .name{color:#fff;font-size:1rem;font-weight:600;margin-right:.3rem}.user-card .user-info .name-row .gender-tag{background:hsla(0,0%,100%,.25);border-radius:50%;color:#fff;font-size:.55rem;height:.9rem;line-height:.9rem;text-align:center;width:.9rem}.user-card .user-info .class-name{color:hsla(0,0%,100%,.8);font-size:.65rem}.user-card .van-icon{flex-shrink:0}.stats-row{display:flex;justify-content:space-around;padding:0 .8rem;position:relative;z-index:1}.stats-row .stat-item{align-items:center;display:flex;flex-direction:column}.stats-row .stat-item .stat-num{color:#fff;font-size:.9rem;font-weight:600;line-height:1.4}.stats-row .stat-item .stat-label{color:hsla(0,0%,100%,.75);font-size:.6rem}.profile-scroll{height:calc(100vh - 2.5rem)}.section-card{background:#fff;border-radius:.4rem;box-shadow:0 .05rem .3rem rgba(0,0,0,.04);margin:.5rem .6rem;overflow:hidden}.section-card .card-header{align-items:center;display:flex;justify-content:space-between;padding:.7rem .6rem .5rem}.section-card .card-header .card-title{color:#323233;font-size:.8rem;font-weight:500}.section-card .card-header .card-more{align-items:center;display:flex}.section-card .card-header .card-more .more-text{color:#969799;font-size:.65rem;margin-right:.1rem}.order-tabs{display:flex;justify-content:space-around;padding:0 .2rem .7rem}.order-tabs .order-tab{align-items:center;display:flex;flex-direction:column;padding:.3rem .2rem}.order-tabs .order-tab .tab-label{color:#646566;font-size:.6rem;margin-top:.25rem}.feature-list{margin-top:.5rem}.feature-list .van-cell-group--inset{margin:0}.feature-list .van-cell{padding:.7rem .6rem}.footer-info{align-items:center;display:flex;flex-direction:column;padding:1rem 0 .5rem}.footer-info .footer-icp{color:#c8c9cc;font-size:.55rem;margin-bottom:.2rem}.footer-info .footer-version{color:#c8c9cc;font-size:.55rem}:root,page{--primary-color:#07c160}
+1
View File
@@ -0,0 +1 @@
.express-card .type-badge.express{background:#e8f4ff;border-radius:.15rem;color:#1989fa;font-size:.55rem;font-weight:500;padding:.1rem .3rem}.secondhand-card .type-badge.secondhand{background:#fff3e8;border-radius:.15rem;color:#ff976a;font-size:.55rem;font-weight:500;padding:.1rem .3rem}.secondhand-card .tag-row{display:flex;gap:.2rem;margin-top:.25rem}.help-card .type-badge.help{background:#fff0f0;border-radius:.15rem;color:#ee0a24;font-size:.55rem;font-weight:500;padding:.1rem .3rem}.activity-card .type-badge.activity{background:#e8fff0;border-radius:.15rem;color:#07c160;font-size:.55rem;font-weight:500;padding:.1rem .3rem}.activity-card .participants-count{color:#646566;font-size:.6rem}.activity-card .progress-bar-wrap{background:#f2f3f5;border-radius:.1rem;height:.2rem;margin:.4rem 0 .2rem;overflow:hidden}.activity-card .progress-bar-wrap .progress-bar{background:linear-gradient(90deg,#07c160,#1989fa);border-radius:.1rem;height:100%;transition:width .3s}.carpool-card .type-badge.carpool{background:#f0e8ff;border-radius:.15rem;color:#7b4ac7;font-size:.55rem;font-weight:500;padding:.1rem .3rem}.carpool-card .carpool-right{align-items:flex-end;display:flex;flex-direction:column}.carpool-card .carpool-right .seat-info{color:#07c160;font-size:.55rem;margin-top:.1rem}.task-card{background:#fff;border-radius:.4rem;box-shadow:0 .05rem .3rem rgba(0,0,0,.04);padding:.6rem}.task-card .card-top{align-items:flex-start;display:flex;justify-content:space-between}.task-card .card-top .task-info{flex:1;margin-right:.4rem;overflow:hidden}.task-card .card-top .task-type-row{align-items:center;display:flex;gap:.2rem;margin-bottom:.3rem}.task-card .card-top .task-title{color:#1a1a1a;display:block;font-size:.8rem;font-weight:500;line-height:1.4;margin-bottom:.2rem}.task-card .card-top .task-desc{-webkit-box-orient:vertical;-webkit-line-clamp:2;color:#969799;display:block;display:-webkit-box;font-size:.65rem;line-height:1.5;overflow:hidden}.task-card .card-top .task-price,.task-card .card-top .task-reward{color:#ee0a24;flex-shrink:0;font-size:.9rem;font-weight:600;white-space:nowrap}.task-card .card-top .task-status{color:#07c160;font-size:.6rem}.task-card .card-bottom{align-items:center;border-top:.025rem solid #f2f3f5;display:flex;justify-content:space-between;margin-top:.5rem;padding-top:.4rem}.task-card .card-bottom .publisher{align-items:center;display:flex}.task-card .card-bottom .publisher .pub-avatar{border-radius:50%;height:.9rem;margin-right:.25rem;width:.9rem}.task-card .card-bottom .publisher .pub-name{color:#323233;font-size:.65rem}.task-card .card-bottom .task-meta{align-items:center;display:flex;gap:.4rem}.task-card .card-bottom .task-meta .meta-item{color:#969799;font-size:.6rem}.tasks-page{background:#f7f8fa;display:flex;flex-direction:column;min-height:100vh}.tasks-search-bar{background:#fff;padding:.5rem .6rem}.tasks-filter{background:#fff;padding-bottom:.4rem}.tasks-filter .filter-scroll{white-space:nowrap}.tasks-filter .filter-tabs{display:flex;gap:.3rem;padding:0 .6rem}.tasks-filter .filter-tab{align-items:center;background:#f7f8fa;border-radius:.4rem;display:flex;flex-direction:column;flex-shrink:0;min-width:3rem;padding:.4rem .5rem;transition:all .2s}.tasks-filter .filter-tab.active{background:#1989fa;box-shadow:0 .1rem .4rem rgba(25,137,250,.3)}.tasks-filter .filter-tab.active .filter-emoji{transform:scale(1.1)}.tasks-filter .filter-tab.active .filter-label{color:#fff}.tasks-filter .filter-tab .filter-emoji{font-size:.9rem;margin-bottom:.15rem;transition:transform .2s}.tasks-filter .filter-tab .filter-label{color:#646566;font-size:.6rem}.tasks-scroll{flex:1}.tasks-list{padding:.4rem .6rem}.tasks-list .task-card-wrap{margin-bottom:.4rem;transition:transform .2s}.tasks-list .task-card-wrap:active{transform:scale(.98)}.tasks-list .empty-tip{color:#969799;font-size:.7rem;padding:3rem 0;text-align:center}.bottom-safe{height:.5rem}:root,page{--primary-color:#07c160}
+1
View File
@@ -0,0 +1 @@
.publish-page{background:#f7f8fa;display:flex;flex-direction:column;min-height:100vh}.publish-nav{backdrop-filter:blur(.5rem);-webkit-backdrop-filter:blur(.5rem);background:hsla(0,0%,100%,.9);border-bottom:.025rem solid rgba(0,0,0,.06);left:0;position:fixed;right:0;top:0;z-index:100}.publish-nav .nav-content{align-items:center;display:flex;height:1.1rem;padding:0 .6rem;position:relative}.publish-nav .nav-content .nav-back{align-items:center;display:flex;height:1.5rem;justify-content:center;margin-left:-.25rem;width:1.5rem}.publish-nav .nav-content .nav-title{color:#1a1a1a;font-size:.8rem;font-weight:500;left:50%;position:absolute;transform:translateX(-50%)}.publish-nav .nav-content .nav-placeholder{flex-shrink:0;width:1.25rem}.publish-scroll{flex:1}.form-card{background:#fff;border-radius:.4rem;box-shadow:0 .05rem .3rem rgba(0,0,0,.04);margin:.4rem .6rem;overflow:hidden}.form-card .card-title{color:#323233;display:block;font-size:.7rem;font-weight:500;margin-bottom:.2rem;padding:.6rem .6rem 0}.reward-hint{color:#969799;font-size:.6rem;padding:0 .6rem .4rem}.bottom-submit-bar{background:#fff;border-top:.025rem solid #f2f3f5;padding:.4rem .6rem}.bottom-submit-bar .submit-btn :global(.van-button){font-size:.8rem;font-weight:500;height:2.2rem}.scroll-bottom-safe{height:4rem}:root,page{--primary-color:#07c160}
+1
View File
@@ -0,0 +1 @@
.shop-card{align-items:stretch;background:#fff;display:flex;padding:.5rem .4rem;transition:background .2s}.shop-card + .shop-card{border-top:.025rem solid #f2f3f5}.shop-card:active{background:#f7f8fa}.shop-image-wrap{border-radius:.2rem;flex-shrink:0;height:4.5rem;margin-right:.5rem;overflow:hidden;width:4.5rem}.shop-image-wrap .shop-image{height:100%;width:100%}.shop-info{display:flex;flex:1;flex-direction:column;justify-content:space-between;min-width:0;padding:.1rem 0}.shop-name-row .shop-name{-webkit-box-orient:vertical;-webkit-line-clamp:1;color:#1a1a1a;display:-webkit-box;font-size:.75rem;font-weight:600;line-height:1.2;overflow:hidden}.shop-rating-row{align-items:center;color:#646566;display:flex;font-size:.6rem;margin-top:.25rem}.shop-rating-row .rating{align-items:center;color:#ff6034;display:flex;font-weight:600}.shop-rating-row .rating .rating-star{font-size:.55rem;margin-right:.05rem}.shop-rating-row .rating .rating-value{font-size:.65rem}.shop-rating-row .sales{color:#646566}.shop-rating-row .delivery-time{color:#1989fa}.shop-rating-row .rating-sep{color:#c8c9cc;margin:0 .2rem}.shop-fee-row{align-items:center;color:#969799;display:flex;font-size:.55rem;margin-top:.2rem}.shop-fee-row .fee-sep{color:#c8c9cc;margin:0 .15rem}.shop-tags{display:flex;flex-wrap:wrap;gap:.2rem;margin-top:.25rem}.shop-tags .shop-tag{margin:0}.shop-desc{-webkit-box-orient:vertical;-webkit-line-clamp:1;color:#969799;display:-webkit-box;font-size:.6rem;line-height:1.4;margin-top:.2rem;overflow:hidden}.food-delivery-page{background:#f7f8fa;display:flex;flex-direction:column;min-height:100vh}.custom-navbar{background:#fff;box-shadow:0 .025rem .1rem rgba(0,0,0,.04)}.navbar-content{align-items:center;display:flex;justify-content:center;padding:0 .6rem}.navbar-title-wrap{align-items:center;display:flex;justify-content:center}.navbar-title{color:#1a1a1a;font-size:.85rem;font-weight:600}.page-scroll{flex:1}.inline-search-section{background:#fff;padding:.5rem .6rem .2rem}.category-section{background:#fff;border-radius:.4rem;box-shadow:0 .05rem .3rem rgba(0,0,0,.04);margin:0 .6rem .3rem;padding:.5rem .2rem .2rem}.shop-list-header{padding:.4rem .6rem .2rem}.shop-list-header .shop-list-title{color:#1a1a1a;font-size:.7rem;font-weight:600}.shop-list-header .shop-list-title .shop-list-count{color:#969799;font-size:.6rem;font-weight:400}.shop-list{background:#fff;border-radius:.4rem;box-shadow:0 .05rem .3rem rgba(0,0,0,.04);margin:0 .6rem;overflow:hidden}.empty-tip{color:#c8c9cc;font-size:.7rem;padding:2rem 0;text-align:center}.bottom-safe{height:1rem}:root,page{--primary-color:#07c160}
+1
View File
@@ -0,0 +1 @@
.article-page{background:#fff;display:flex;flex-direction:column;min-height:100vh}.article-nav{backdrop-filter:blur(.5rem);-webkit-backdrop-filter:blur(.5rem);background:hsla(0,0%,100%,.9);border-bottom:.025rem solid rgba(0,0,0,.06);left:0;position:fixed;right:0;top:0;z-index:100}.article-nav .nav-content{align-items:center;display:flex;height:1.1rem;padding:0 .6rem;position:relative}.article-nav .nav-content .nav-back{align-items:center;display:flex;height:1.5rem;justify-content:center;margin-left:-.25rem;width:1.5rem}.article-nav .nav-content .nav-title{color:#1a1a1a;font-size:.8rem;font-weight:500;left:50%;position:absolute;transform:translateX(-50%)}.article-nav .nav-content .nav-placeholder{flex-shrink:0;width:1.25rem}.article-scroll{flex:1}.article-content{padding:.6rem .8rem}.article-title{color:#1a1a1a;display:block;font-size:1rem;font-weight:600;line-height:1.4;margin-bottom:.5rem}.article-meta{align-items:center;border-bottom:.025rem solid #f2f3f5;color:#969799;display:flex;font-size:.65rem;margin-bottom:.8rem;padding-bottom:.6rem}.article-meta .meta-source{color:#1989fa;margin-right:.5rem}.article-meta .meta-time{margin-right:.5rem}.article-meta .meta-read{flex:1;text-align:right}.article-body{margin-bottom:.6rem}.article-paragraph{color:#323233;display:block;font-size:.8rem;line-height:1.85;margin-bottom:.5rem;text-align:justify}.article-images{margin-bottom:.6rem}.article-images .article-img{border-radius:.2rem;margin-bottom:.4rem;width:100%}.action-bar{align-items:center;background:#f7f8fa;border-radius:.4rem;display:flex;justify-content:space-around;margin:0 .8rem;padding:.7rem .8rem}.action-bar .action-item{align-items:center;display:flex;gap:.2rem}.action-bar .action-item .action-text{color:#646566;font-size:.7rem}.comment-section{margin-top:.8rem;padding:0 .8rem}.comment-header{align-items:baseline;display:flex;margin-bottom:.6rem}.comment-header .comment-title{color:#1a1a1a;font-size:.85rem;font-weight:600}.comment-header .comment-count{color:#969799;font-size:.7rem;margin-left:.2rem}.comment-list .comment-item{border-bottom:.025rem solid #f2f3f5;display:flex;padding:.6rem 0}.comment-list .comment-item .comment-avatar{border-radius:50%;flex-shrink:0;height:1.8rem;width:1.8rem}.comment-list .comment-item .comment-body{flex:1;margin-left:.5rem}.comment-list .comment-item .comment-top{align-items:center;display:flex;justify-content:space-between;margin-bottom:.25rem}.comment-list .comment-item .comment-top .comment-name{color:#323233;font-size:.7rem;font-weight:500}.comment-list .comment-item .comment-top .comment-time{color:#c8c9cc;font-size:.6rem}.comment-list .comment-item .comment-text{color:#323233;display:block;font-size:.75rem;line-height:1.6;margin-bottom:.3rem}.comment-list .comment-item .comment-actions{align-items:center;display:flex}.comment-list .comment-item .comment-actions .comment-like{align-items:center;display:flex;gap:.1rem;padding:.1rem 0}.comment-list .comment-item .comment-actions .comment-like .like-count{color:#969799;font-size:.6rem}.comment-list .comment-item .comment-actions .comment-like .like-count.liked{color:#ee0a24}.comment-list .reply-list{background:#f7f8fa;border-radius:.3rem;margin-top:.4rem;padding:.4rem .5rem}.comment-list .reply-item{display:flex;padding:.3rem 0}.comment-list .reply-item + .reply-item{border-top:.025rem solid #ebedf0}.comment-list .reply-item .reply-avatar{border-radius:50%;flex-shrink:0;height:1.4rem;width:1.4rem}.no-comments{color:#c8c9cc;font-size:.7rem;padding:1.5rem 0;text-align:center}.comment-input-bar{align-items:center;background:#fff;border-top:.025rem solid #f2f3f5;display:flex;gap:.4rem;padding:.4rem .6rem}.comment-input-bar .input-wrap{background:#f7f8fa;border-radius:1rem;flex:1;padding:.3rem .6rem}.comment-input-bar .input-wrap .comment-input{font-size:.7rem;height:1.1rem;line-height:1.1rem}.comment-input-bar .send-btn{background:#1989fa;border-radius:1rem;flex-shrink:0;padding:.3rem .7rem}.comment-input-bar .send-btn .send-text{color:#fff;font-size:.7rem;font-weight:500}.comment-bottom-safe{height:2.5rem}.empty-article-wrap{align-items:center;display:flex;flex-direction:column;justify-content:center;padding:5rem .8rem 0}.empty-article-wrap .empty-article-icon{font-size:2rem;margin-bottom:.6rem;opacity:.4}.empty-article-wrap .empty-article-text{color:#969799;font-size:.75rem;margin-bottom:.8rem}.empty-article-wrap .empty-article-back-btn{background:#1989fa;border-radius:1rem;padding:.4rem 1.2rem}.empty-article-wrap .empty-article-back-text{color:#fff;font-size:.7rem;font-weight:500}:root,page{--primary-color:#07c160}
+1
View File
@@ -0,0 +1 @@
.forum-post-card{background:#fff;border-radius:.4rem;box-shadow:0 .05rem .2rem rgba(0,0,0,.04);padding:.6rem .6rem .4rem;transition:transform .15s}.forum-post-card:active{transform:scale(.99)}.post-header{align-items:center;display:flex;margin-bottom:.4rem}.author-avatar-wrap{background:#f0f2f5;border-radius:50%;flex-shrink:0;height:2rem;overflow:hidden;width:2rem}.author-avatar-wrap .author-avatar{height:100%;width:100%}.author-avatar-wrap .author-avatar-fallback{align-items:center;background:linear-gradient(135deg,#1989fa,#5dabff);display:flex;height:100%;justify-content:center;width:100%}.author-avatar-wrap .avatar-letter{color:#fff;font-size:.8rem;font-weight:600}.author-info{flex:1;margin-left:.4rem;min-width:0}.author-info .author-name{color:#1a1a1a;display:block;font-size:.7rem;font-weight:600;line-height:1.3}.author-info .post-time{color:#969799;display:block;font-size:.55rem;margin-top:.1rem}.follow-btn{background:#fff;border:.025rem solid #1989fa;border-radius:.8rem;flex-shrink:0;padding:.2rem .5rem}.follow-btn .follow-text{color:#1989fa;font-size:.55rem;font-weight:500}.post-body{margin-bottom:.4rem}.post-title{color:#1a1a1a;display:block;font-size:.8rem;font-weight:600;line-height:1.4;margin-bottom:.3rem}.post-excerpt{-webkit-box-orient:vertical;-webkit-line-clamp:3;color:#646566;display:block;display:-webkit-box;font-size:.65rem;line-height:1.6;overflow:hidden}.post-images{display:grid;gap:.2rem;margin-top:.4rem}.images-1{grid-template-columns:1fr}.images-1 .post-image-wrap{aspect-ratio:16/10;max-height:9rem}.images-2{grid-template-columns:1fr 1fr}.images-2 .post-image-wrap{aspect-ratio:1}.images-3{grid-template-columns:1fr 1fr 1fr}.images-3 .post-image-wrap{aspect-ratio:1}.post-image-wrap{background:#f0f2f5;border-radius:.2rem;overflow:hidden;position:relative}.post-image{height:100%;width:100%}.image-more-mask{align-items:center;background:rgba(0,0,0,.5);display:flex;inset:0;justify-content:center;position:absolute}.image-more-text{color:#fff;font-size:1rem;font-weight:600}.post-footer{align-items:center;border-top:.025rem solid #f2f3f5;display:flex;padding-top:.3rem}.action-item{align-items:center;display:flex;margin-right:.8rem}.action-item.right{margin-left:auto;margin-right:0}.action-item .action-icon{font-size:.7rem;line-height:1;margin-right:.15rem}.action-item .action-icon.share{color:#969799}.action-item .action-count{color:#646566;font-size:.6rem;line-height:1}.forum-page{background:#f7f8fa;display:flex;flex-direction:column;min-height:100vh}.custom-navbar{background:#fff;box-shadow:0 .025rem .1rem rgba(0,0,0,.04)}.navbar-content{align-items:center;display:flex;padding:0 .4rem;position:relative}.navbar-title-wrap{align-items:center;display:flex;flex:1;justify-content:center}.navbar-title{color:#1a1a1a;font-size:.85rem;font-weight:600}.navbar-publish{align-items:center;background:#f7f8fa;border-radius:50%;display:flex;flex-shrink:0;height:1.5rem;justify-content:center;position:absolute;right:.4rem;top:50%;transform:translateY(-50%);width:1.5rem}.navbar-publish:active{opacity:.7}.navbar-publish .publish-icon{font-size:.75rem}.page-scroll{flex:1}.inline-search-section{background:#fff;padding:.5rem .6rem .2rem}.category-section{background:#fff;border-radius:.4rem;box-shadow:0 .05rem .3rem rgba(0,0,0,.04);margin:0 .6rem .3rem;padding:.6rem .2rem .4rem}.post-list-header{padding:.4rem .6rem .3rem}.post-list-header .post-list-title{color:#1a1a1a;font-size:.7rem;font-weight:600}.post-list-header .post-list-title .post-list-count{color:#969799;font-size:.6rem;font-weight:400}.post-list{display:flex;flex-direction:column;gap:.5rem;padding:0 .6rem}.empty-tip{color:#c8c9cc;font-size:.7rem;padding:2rem 0;text-align:center}.bottom-safe{height:1rem}:root,page{--primary-color:#07c160}
+1
View File
@@ -0,0 +1 @@
.chat-page{background:#f7f8fa;display:flex;flex-direction:column;min-height:100vh}.chat-nav{backdrop-filter:blur(.5rem);-webkit-backdrop-filter:blur(.5rem);background:hsla(0,0%,100%,.9);border-bottom:.025rem solid rgba(0,0,0,.06);left:0;position:fixed;right:0;top:0;z-index:100}.chat-nav .nav-content{align-items:center;display:flex;height:1.1rem;padding:0 .6rem;position:relative}.chat-nav .nav-content .nav-back{align-items:center;display:flex;height:1.5rem;justify-content:center;margin-left:-.25rem;width:1.5rem}.chat-nav .nav-content .nav-info{align-items:center;display:flex;flex-direction:column;left:50%;position:absolute;transform:translateX(-50%)}.chat-nav .nav-content .nav-info .nav-name{color:#1a1a1a;font-size:.8rem;font-weight:500}.chat-nav .nav-content .nav-info .nav-role{color:#969799;font-size:.55rem}.chat-nav .nav-content .nav-placeholder{flex-shrink:0;width:1.25rem}.chat-scroll{flex:1}.message-list{padding:.4rem .6rem}.message-item{display:flex;flex-direction:column;margin-bottom:.6rem}.message-item.received{align-items:flex-start}.message-item.received .bubble{background:#fff;box-shadow:0 .05rem .2rem rgba(0,0,0,.06);color:#323233}.message-item.sent{align-items:flex-end}.message-item.sent .bubble{background:linear-gradient(135deg,#1989fa,#06b7f0);color:#fff}.message-item .bubble{border-radius:.4rem;font-size:.75rem;line-height:1.5;max-width:70%;padding:.5rem .6rem;word-break:break-all}.message-item .msg-time{color:#c8c9cc;font-size:.55rem;margin-top:.2rem;padding:0 .1rem}.chat-input-bar{align-items:center;background:#fff;border-top:.025rem solid #f2f3f5;display:flex;gap:.4rem;padding:.4rem .6rem}.chat-input-bar .input-wrap{background:#f7f8fa;border-radius:1rem;flex:1;padding:.3rem .6rem}.chat-input-bar .input-wrap .chat-input{font-size:.7rem;height:1.1rem;line-height:1.1rem}.chat-input-bar .send-btn{background:#1989fa;border-radius:1rem;flex-shrink:0;padding:.3rem .7rem}.chat-input-bar .send-btn .send-text{color:#fff;font-size:.7rem;font-weight:500}.scroll-bottom-safe{height:3rem}:root,page{--primary-color:#07c160}
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
.system-msg-page{background:#f7f8fa;display:flex;flex-direction:column;min-height:100vh}.sys-nav{backdrop-filter:blur(.5rem);-webkit-backdrop-filter:blur(.5rem);background:hsla(0,0%,100%,.9);border-bottom:.025rem solid rgba(0,0,0,.06);left:0;position:fixed;right:0;top:0;z-index:100}.sys-nav .nav-content{align-items:center;display:flex;height:1.1rem;padding:0 .6rem;position:relative}.sys-nav .nav-content .nav-back{align-items:center;display:flex;height:1.5rem;justify-content:center;margin-left:-.25rem;width:1.5rem}.sys-nav .nav-content .nav-title{color:#1a1a1a;font-size:.8rem;font-weight:500;left:50%;position:absolute;transform:translateX(-50%)}.sys-nav .nav-content .nav-placeholder{flex-shrink:0;width:1.25rem}.sys-scroll{flex:1}.sys-list{padding:.4rem .6rem}.sys-card{background:#fff;border-radius:.4rem;box-shadow:0 .05rem .3rem rgba(0,0,0,.04);margin-bottom:.4rem;padding:.6rem}.sys-card .sys-card-header{align-items:center;display:flex;margin-bottom:.4rem}.sys-card .sys-card-header .sys-card-icon{align-items:center;background:linear-gradient(135deg,#1989fa,#07c160);border-radius:50%;color:#fff;display:flex;flex-shrink:0;font-size:.9rem;height:1.8rem;justify-content:center;width:1.8rem}.sys-card .sys-card-header .sys-card-title{color:#1a1a1a;font-size:.75rem;font-weight:500;margin-left:.4rem}.sys-card .sys-card-content{color:#646566;font-size:.7rem;line-height:1.7;margin-bottom:.3rem}.sys-card .sys-card-time{color:#c8c9cc;font-size:.6rem;text-align:right}:root,page{--primary-color:#07c160}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
.publish-page{background:#f7f8fa;display:flex;flex-direction:column;min-height:100vh}.publish-nav{backdrop-filter:blur(.5rem);-webkit-backdrop-filter:blur(.5rem);background:hsla(0,0%,100%,.9);border-bottom:.025rem solid rgba(0,0,0,.06);left:0;position:fixed;right:0;top:0;z-index:100}.publish-nav .nav-content{align-items:center;display:flex;height:1.1rem;padding:0 .6rem;position:relative}.publish-nav .nav-content .nav-back{align-items:center;display:flex;height:1.5rem;justify-content:center;margin-left:-.25rem;width:1.5rem}.publish-nav .nav-content .nav-title{color:#1a1a1a;font-size:.8rem;font-weight:500;left:50%;position:absolute;transform:translateX(-50%)}.publish-nav .nav-content .nav-placeholder{flex-shrink:0;width:1.25rem}.publish-scroll{flex:1}.form-card{background:#fff;border-radius:.4rem;box-shadow:0 .05rem .3rem rgba(0,0,0,.04);margin:.4rem .6rem;overflow:hidden}.form-card .card-title{color:#323233;display:block;font-size:.7rem;font-weight:500;margin-bottom:.2rem;padding:.6rem .6rem 0}.upload-section{padding:.4rem .6rem .6rem}.upload-section .upload-label{color:#969799;display:block;font-size:.65rem;margin-bottom:.3rem}.tag-hint{color:#969799;font-size:.6rem;padding:0 .6rem .4rem}.bottom-submit-bar{background:#fff;border-top:.025rem solid #f2f3f5;padding:.4rem .6rem}.bottom-submit-bar .submit-btn :global(.van-button){font-size:.8rem;font-weight:500;height:2.2rem}.scroll-bottom-safe{height:4rem}:root,page{--primary-color:#07c160}
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
.publish-page{background:#f7f8fa;display:flex;flex-direction:column;min-height:100vh}.publish-nav{backdrop-filter:blur(.5rem);-webkit-backdrop-filter:blur(.5rem);background:hsla(0,0%,100%,.9);border-bottom:.025rem solid rgba(0,0,0,.06);left:0;position:fixed;right:0;top:0;z-index:100}.publish-nav .nav-content{align-items:center;display:flex;height:1.1rem;padding:0 .6rem;position:relative}.publish-nav .nav-content .nav-back{align-items:center;display:flex;height:1.5rem;justify-content:center;margin-left:-.25rem;width:1.5rem}.publish-nav .nav-content .nav-title{color:#1a1a1a;font-size:.8rem;font-weight:500;left:50%;position:absolute;transform:translateX(-50%)}.publish-nav .nav-content .nav-placeholder{flex-shrink:0;width:1.25rem}.publish-scroll{flex:1}.form-card{background:#fff;border-radius:.4rem;box-shadow:0 .05rem .3rem rgba(0,0,0,.04);margin:.4rem .6rem;overflow:hidden}.form-card .card-title{color:#323233;display:block;font-size:.7rem;font-weight:500;margin-bottom:.2rem;padding:.6rem .6rem 0}.upload-section{padding:.4rem .6rem .6rem}.upload-section .upload-label{color:#969799;display:block;font-size:.65rem;margin-bottom:.3rem}.bottom-submit-bar{background:#fff;border-top:.025rem solid #f2f3f5;padding:.4rem .6rem}.bottom-submit-bar .submit-btn :global(.van-button){font-size:.8rem;font-weight:500;height:2.2rem}.scroll-bottom-safe{height:4rem}:root,page{--primary-color:#07c160}
+1
View File
@@ -0,0 +1 @@
.messages-page{background:#f7f8fa;display:flex;flex-direction:column;min-height:100vh}.messages-scroll{flex:1}.section-block{background:#fff;border-radius:.4rem;box-shadow:0 .05rem .3rem rgba(0,0,0,.04);margin:.5rem .6rem 0;overflow:hidden}.section-block .section-title{color:#323233;font-size:.7rem;font-weight:500;padding:.6rem .6rem .3rem}.message-item{align-items:center;display:flex;padding:.6rem;transition:background .15s}.message-item:active{background:#f7f8fa}.message-item + .message-item{border-top:.025rem solid #f2f3f5}.system-msg .sys-icon{align-items:center;background:linear-gradient(135deg,#1989fa,#07c160);border-radius:50%;color:#fff;display:flex;flex-shrink:0;font-size:.9rem;height:2.2rem;justify-content:center;width:2.2rem}.system-msg .pinned-tag{align-self:flex-start;background:#e8f4ff;border-radius:.1rem;color:#1989fa;flex-shrink:0;font-size:.5rem;margin-top:.1rem;padding:.1rem .25rem}.chat-msg .avatar-wrap{flex-shrink:0;position:relative}.chat-msg .avatar-wrap .msg-avatar{border-radius:50%;height:2.4rem;width:2.4rem}.chat-msg .avatar-wrap .online-dot{background:#07c160;border:.075rem solid #fff;border-radius:50%;bottom:.05rem;height:.45rem;position:absolute;right:.05rem;width:.45rem}.msg-body{flex:1;margin-left:.5rem;overflow:hidden}.msg-body .msg-top{align-items:center;display:flex;justify-content:space-between;margin-bottom:.2rem}.msg-body .msg-top .msg-name-row{align-items:center;display:flex}.msg-body .msg-top .msg-name{color:#1a1a1a;font-size:.75rem;font-weight:500}.msg-body .msg-top .msg-time{color:#c8c9cc;flex-shrink:0;font-size:.55rem}.msg-body .msg-preview{color:#969799;display:block;font-size:.65rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.role-tag{border-radius:.1rem;font-size:.5rem;margin-left:.25rem;padding:.05rem .2rem}.role-tag.role-rider{background:#fff3e8;color:#ff976a}.role-tag.role-merchant{background:#e8f4ff;color:#1989fa}.role-tag.role-user{background:#f2f3f5;color:#646566}.bottom-safe{height:.5rem}:root,page{--primary-color:#07c160}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+9
View File
@@ -0,0 +1,9 @@
/**
* @license React
* react-jsx-runtime.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
File diff suppressed because one or more lines are too long
+9
View File
@@ -0,0 +1,9 @@
/**
* @license React
* react-jsx-runtime.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
File diff suppressed because one or more lines are too long
+39
View File
@@ -0,0 +1,39 @@
/**
* @license React
* react-jsx-runtime.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* @license React
* react-reconciler-constants.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* @license React
* react-reconciler.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* @license React
* scheduler.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
File diff suppressed because one or more lines are too long
+9
View File
@@ -0,0 +1,9 @@
/**
* @license React
* react-jsx-runtime.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
File diff suppressed because one or more lines are too long
+9
View File
@@ -0,0 +1,9 @@
/**
* @license React
* react-jsx-runtime.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
File diff suppressed because one or more lines are too long
+9
View File
@@ -0,0 +1,9 @@
/**
* @license React
* react-jsx-runtime.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
File diff suppressed because one or more lines are too long
+9
View File
@@ -0,0 +1,9 @@
/**
* @license React
* react-jsx-runtime.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
File diff suppressed because one or more lines are too long
+45
View File
@@ -0,0 +1,45 @@
/*!
Copyright (c) 2018 Jed Watson.
Licensed under the MIT License (MIT), see
http://jedwatson.github.io/classnames
*/
/**
* @license React
* react-jsx-runtime.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* @license React
* react-reconciler-constants.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* @license React
* react-reconciler.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* @license React
* scheduler.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
File diff suppressed because one or more lines are too long
+9
View File
@@ -0,0 +1,9 @@
/**
* @license React
* react-jsx-runtime.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
+2
View File
File diff suppressed because one or more lines are too long
+26
View File
@@ -0,0 +1,26 @@
/*!
Copyright (c) 2018 Jed Watson.
Licensed under the MIT License (MIT), see
http://jedwatson.github.io/classnames
*/
/*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/facebook/regenerator/blob/main/LICENSE */
/**
* @license React
* react-jsx-runtime.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/** @license React v16.13.1
* react-is.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
File diff suppressed because one or more lines are too long
+50
View File
@@ -0,0 +1,50 @@
/*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/facebook/regenerator/blob/main/LICENSE */
/**
* @license React
* react-jsx-runtime.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* @license React
* react-reconciler-constants.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* @license React
* react-reconciler.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* @license React
* scheduler.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/** @license React v16.13.1
* react-is.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
File diff suppressed because one or more lines are too long
+39
View File
@@ -0,0 +1,39 @@
/**
* @license React
* react-jsx-runtime.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* @license React
* react-reconciler-constants.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* @license React
* react-reconciler.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* @license React
* scheduler.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
File diff suppressed because one or more lines are too long
+9
View File
@@ -0,0 +1,9 @@
/**
* @license React
* react-jsx-runtime.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
File diff suppressed because one or more lines are too long
+11
View File
@@ -0,0 +1,11 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*
* This file is a port of shadowCSS from webcomponents.js to TypeScript.
* https://github.com/webcomponents/webcomponentsjs/blob/4efecd7e0e/src/ShadowCSS/ShadowCSS.js
* https://github.com/angular/angular/blob/master/packages/compiler/src/shadow_css.ts
*/
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+39
View File
@@ -0,0 +1,39 @@
/**
* @license React
* react-jsx-runtime.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* @license React
* react-reconciler-constants.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* @license React
* react-reconciler.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* @license React
* scheduler.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
File diff suppressed because one or more lines are too long
+15
View File
@@ -0,0 +1,15 @@
/*!
Copyright (c) 2018 Jed Watson.
Licensed under the MIT License (MIT), see
http://jedwatson.github.io/classnames
*/
/**
* @license React
* react-jsx-runtime.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
+1
View File
@@ -0,0 +1 @@
<!doctype html><html><head><meta content="text/html; charset=utf-8" http-equiv="Content-Type"><meta content="width=device-width,initial-scale=1,user-scalable=no" name="viewport"><meta name="apple-mobile-web-app-capable" content="yes"><meta name="apple-touch-fullscreen" content="yes"><meta name="format-detection" content="telephone=no,address=no"><meta name="apple-mobile-web-app-status-bar-style" content="white"><meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"><title>antmjs</title><script crossorigin="anonymous" src="https://polyfill.alicdn.com/polyfill.min.js?features=es2015%2Ces2016%2Ces2017%2Ces2018%2Ces2019%2Ces2020%2Ces2021%2Ces2022"></script><script>!function(n){function f(){var e=n.document.documentElement,w=e.getBoundingClientRect().width,x=40*w/750;e.style.fontSize=x>=40?"40px":x<=20?"20px":x+"px"}n.addEventListener("resize",(function(){f()})),f()}(window);</script><script defer="defer" src="/js/917.js"></script><script defer="defer" src="/js/app.js"></script><link href="/assets/css/app.css" rel="stylesheet"></head><body><div id="app"></div></body></html>
+2
View File
File diff suppressed because one or more lines are too long
+5
View File
@@ -0,0 +1,5 @@
/*!
Copyright (c) 2018 Jed Watson.
Licensed under the MIT License (MIT), see
http://jedwatson.github.io/classnames
*/
+2
View File
File diff suppressed because one or more lines are too long
+37
View File
@@ -0,0 +1,37 @@
/*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/facebook/regenerator/blob/main/LICENSE */
/*!@license Copyright 2013, Heinrich Goebl, License: MIT, see https://github.com/hgoebl/mobile-detect.js*/
/*!__STENCIL_STATIC_IMPORT_SWITCH__*/
/*!mobile-detect v1.4.5 2021-03-13*/
/**
* @license React
* react-dom.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* @license React
* react.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* @license React
* scheduler.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
+4 -1
View File
@@ -1,6 +1,10 @@
export default defineAppConfig({
pages: [
'pages/index/index',
'pages/food-delivery/index',
'pages/shop-detail/index',
'pages/forum/index',
'pages/search/index',
'pages/tasks/index',
'pages/task-detail/index',
'pages/publish-article/index',
@@ -8,7 +12,6 @@ export default defineAppConfig({
'pages/publish-secondhand/index',
'pages/chat/index',
'pages/system-messages/index',
'pages/product-detail/index',
'pages/messages/index',
'pages/profile/index',
'pages/article/index',
@@ -40,8 +40,11 @@
}
.meta-read {
flex: 1;
text-align: right;
margin-left: auto;
}
.meta-comment {
margin-left: 16px;
}
}
}
@@ -26,6 +26,9 @@ export default function ImageGridCard({ article }: ImageGridCardProps) {
<Text className='meta-source'>{article.source}</Text>
<Text className='meta-time'>{article.time}</Text>
<Text className='meta-read'>{article.readCount}</Text>
{typeof article.commentCount === 'number' && (
<Text className='meta-comment'>💬 {article.commentCount}</Text>
)}
</View>
</View>
)
@@ -56,8 +56,11 @@
}
.meta-read {
flex: 1;
text-align: right;
margin-left: auto;
}
.meta-comment {
margin-left: 16px;
}
}
}
@@ -21,6 +21,9 @@ export default function ImageTextCard({ article }: ImageTextCardProps) {
<Text className='meta-source'>{article.source}</Text>
<Text className='meta-time'>{article.time}</Text>
<Text className='meta-read'>{article.readCount}</Text>
{typeof article.commentCount === 'number' && (
<Text className='meta-comment'>💬 {article.commentCount}</Text>
)}
</View>
</View>
</View>
@@ -37,8 +37,11 @@
}
.meta-read {
flex: 1;
text-align: right;
margin-left: auto;
}
.meta-comment {
margin-left: 16px;
}
}
}
@@ -15,6 +15,9 @@ export default function TextCard({ article }: TextCardProps) {
<Text className='meta-source'>{article.source}</Text>
<Text className='meta-time'>{article.time}</Text>
<Text className='meta-read'>{article.readCount}</Text>
{typeof article.commentCount === 'number' && (
<Text className='meta-comment'>💬 {article.commentCount}</Text>
)}
</View>
</View>
)
+202
View File
@@ -0,0 +1,202 @@
/* ========================================
论坛帖卡(社交化:小红书/即刻 风格)
======================================== */
.forum-post-card {
background: #fff;
border-radius: 16px;
padding: 24px 24px 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
transition: transform 0.15s;
&:active {
transform: scale(0.99);
}
}
/* ========== 用户头 ========== */
.post-header {
display: flex;
align-items: center;
margin-bottom: 16px;
}
.author-avatar-wrap {
width: 80px;
height: 80px;
border-radius: 50%;
overflow: hidden;
flex-shrink: 0;
background: #f0f2f5;
.author-avatar {
width: 100%;
height: 100%;
}
.author-avatar-fallback {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #1989fa, #5dabff);
}
.avatar-letter {
font-size: 32px;
color: #fff;
font-weight: 600;
}
}
.author-info {
flex: 1;
margin-left: 16px;
min-width: 0;
.author-name {
display: block;
font-size: 28px;
font-weight: 600;
color: #1a1a1a;
line-height: 1.3;
}
.post-time {
display: block;
font-size: 22px;
color: #969799;
margin-top: 4px;
}
}
.follow-btn {
flex-shrink: 0;
padding: 8px 20px;
border: 1px solid #1989fa;
border-radius: 32px;
background: #fff;
.follow-text {
font-size: 22px;
color: #1989fa;
font-weight: 500;
}
}
/* ========== 内容区 ========== */
.post-body {
margin-bottom: 16px;
}
.post-title {
display: block;
font-size: 32px;
font-weight: 600;
color: #1a1a1a;
line-height: 1.4;
margin-bottom: 12px;
}
.post-excerpt {
display: block;
font-size: 26px;
color: #646566;
line-height: 1.6;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
overflow: hidden;
}
/* ========== 图片网格 ========== */
.post-images {
margin-top: 16px;
display: grid;
gap: 8px;
}
.images-1 {
grid-template-columns: 1fr;
.post-image-wrap {
aspect-ratio: 16 / 10;
max-height: 360px;
}
}
.images-2 {
grid-template-columns: 1fr 1fr;
.post-image-wrap {
aspect-ratio: 1;
}
}
.images-3 {
grid-template-columns: 1fr 1fr 1fr;
.post-image-wrap {
aspect-ratio: 1;
}
}
.post-image-wrap {
position: relative;
border-radius: 8px;
overflow: hidden;
background: #f0f2f5;
}
.post-image {
width: 100%;
height: 100%;
}
.image-more-mask {
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
}
.image-more-text {
font-size: 40px;
font-weight: 600;
color: #fff;
}
/* ========== 底部操作栏 ========== */
.post-footer {
display: flex;
align-items: center;
padding-top: 12px;
border-top: 1px solid #f2f3f5;
}
.action-item {
display: flex;
align-items: center;
margin-right: 32px;
&.right {
margin-right: 0;
margin-left: auto;
}
.action-icon {
font-size: 28px;
margin-right: 6px;
line-height: 1;
}
.action-icon.share {
color: #969799;
}
.action-count {
font-size: 24px;
color: #646566;
line-height: 1;
}
}
+113
View File
@@ -0,0 +1,113 @@
import { View, Text, Image } from '@tarojs/components'
import type { Article } from '@/types/article'
import './index.less'
interface Props {
post: Article
onClick?: (post: Article) => void
}
/** 数字超过 1000 显示为 1k 形式 */
function formatCount(n: number | undefined): string {
if (n === undefined) return '0'
if (n < 1000) return String(n)
if (n < 10000) return `${(n / 1000).toFixed(1)}k`
return `${(n / 10000).toFixed(1)}w`
}
export default function ForumPostCard({ post, onClick }: Props) {
const imageCount = post.images.length
// 单图时使用第一张;多图时最多展示 3 张
const displayImages = imageCount === 1 ? post.images : post.images.slice(0, 3)
return (
<View className='forum-post-card' onClick={() => onClick?.(post)}>
{/* ========== 用户头 ========== */}
<View className='post-header'>
<View className='author-avatar-wrap'>
{post.sourceAvatar ? (
<Image
className='author-avatar'
src={post.sourceAvatar}
mode='aspectFill'
/>
) : (
<View className='author-avatar-fallback'>
<Text className='avatar-letter'>
{post.source.charAt(0)}
</Text>
</View>
)}
</View>
<View className='author-info'>
<Text className='author-name' numberOfLines={1}>
{post.source}
</Text>
<Text className='post-time'>{post.time}</Text>
</View>
<View
className='follow-btn'
onClick={(e) => {
e?.stopPropagation?.()
}}
>
<Text className='follow-text'>+ </Text>
</View>
</View>
{/* ========== 内容区 ========== */}
<View className='post-body'>
{/* 标题 */}
<Text className='post-title' numberOfLines={2}>
{post.title}
</Text>
{/* 摘要 */}
<Text className='post-excerpt' numberOfLines={3}>
{post.excerpt}
</Text>
{/* 图片 */}
{imageCount > 0 && (
<View
className={`post-images images-${Math.min(imageCount, 3)}`}
>
{displayImages.map((img, idx) => (
<View key={idx} className='post-image-wrap'>
<Image
className='post-image'
src={img}
mode='aspectFill'
/>
{imageCount > 3 && idx === 2 && (
<View className='image-more-mask'>
<Text className='image-more-text'>+{imageCount - 3}</Text>
</View>
)}
</View>
))}
</View>
)}
</View>
{/* ========== 底部操作栏 ========== */}
<View className='post-footer'>
<View className='action-item'>
<Text className='action-icon'>💬</Text>
<Text className='action-count'>{formatCount(post.commentCount)}</Text>
</View>
<View className='action-item'>
<Text className='action-icon'></Text>
<Text className='action-count'>{formatCount(post.likeCount)}</Text>
</View>
<View className='action-item'>
<Text className='action-icon'>👁</Text>
<Text className='action-count'>{formatCount(post.readCount)}</Text>
</View>
<View className='action-item right'>
<Text className='action-icon share'></Text>
</View>
</View>
</View>
)
}
-92
View File
@@ -1,92 +0,0 @@
/* ========================================
商品卡片(双列网格用)
======================================== */
.product-card {
background: #fff;
border-radius: 16px;
overflow: hidden;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
transition: transform 0.2s;
&:active {
transform: scale(0.97);
}
}
/* ========== 商品图片 ========== */
.product-image-wrap {
position: relative;
width: 100%;
height: 340px;
overflow: hidden;
.product-image {
width: 100%;
height: 100%;
}
.discount-tag {
position: absolute;
top: 12px;
left: 12px;
background: linear-gradient(135deg, #ee0a24, #ff6034);
color: #fff;
font-size: 22px;
font-weight: 600;
padding: 4px 10px;
border-radius: 6px;
}
}
/* ========== 商品信息 ========== */
.product-info {
padding: 16px;
.product-title {
display: block;
font-size: 28px;
font-weight: 500;
color: #1a1a1a;
line-height: 1.4;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
margin-bottom: 8px;
}
.product-tags {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-bottom: 10px;
}
.product-price-row {
display: flex;
align-items: baseline;
gap: 8px;
margin-bottom: 6px;
.product-price {
font-size: 36px;
font-weight: 700;
color: #ee0a24;
line-height: 1;
}
.product-original-price {
font-size: 24px;
color: #c8c9cc;
text-decoration: line-through;
}
}
.product-meta {
.product-sales {
font-size: 22px;
color: #969799;
}
}
}
-62
View File
@@ -1,62 +0,0 @@
import { View, Text } from '@tarojs/components'
import { Image as VantImage, Tag } from '@antmjs/vantui'
import type { Product } from '@/types/product'
import './index.less'
interface Props {
product: Product
onClick?: (product: Product) => void
}
export default function ProductCard({ product, onClick }: Props) {
const discount = product.originalPrice
? Math.round((product.price / product.originalPrice) * 10)
: null
return (
<View className='product-card' onClick={() => onClick?.(product)}>
{/* 商品图片 */}
<View className='product-image-wrap'>
<VantImage
className='product-image'
src={product.image}
fit='cover'
width='100%'
height='100%'
radius={12}
/>
{discount && discount < 10 && (
<View className='discount-tag'>{discount}</View>
)}
</View>
{/* 商品信息 */}
<View className='product-info'>
<Text className='product-title'>{product.title}</Text>
{product.tags && product.tags.length > 0 && (
<View className='product-tags'>
{product.tags.slice(0, 2).map((t) => (
<Tag key={t} type='danger' plain size='medium'>
{t}
</Tag>
))}
</View>
)}
<View className='product-price-row'>
<Text className='product-price'>¥{product.price.toFixed(1)}</Text>
{product.originalPrice && (
<Text className='product-original-price'>
¥{product.originalPrice.toFixed(1)}
</Text>
)}
</View>
<View className='product-meta'>
<Text className='product-sales'>{product.sales}</Text>
</View>
</View>
</View>
)
}
+135
View File
@@ -0,0 +1,135 @@
/* ========================================
商家卡片(单列,左图右信息)
仿美团外卖商家卡片
======================================== */
.shop-card {
display: flex;
align-items: stretch;
padding: 20px 16px;
background: #fff;
transition: background 0.2s;
& + .shop-card {
border-top: 1px solid #f2f3f5;
}
&:active {
background: #f7f8fa;
}
}
/* ========== 左侧图片 ========== */
.shop-image-wrap {
flex-shrink: 0;
width: 180px;
height: 180px;
margin-right: 20px;
border-radius: 8px;
overflow: hidden;
.shop-image {
width: 100%;
height: 100%;
}
}
/* ========== 右侧信息 ========== */
.shop-info {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 4px 0;
}
/* 商家名 */
.shop-name-row {
.shop-name {
font-size: 30px;
font-weight: 600;
color: #1a1a1a;
line-height: 1.2;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
overflow: hidden;
}
}
/* 评分 + 月售 + 配送时间 */
.shop-rating-row {
display: flex;
align-items: center;
margin-top: 10px;
font-size: 24px;
color: #646566;
.rating {
display: flex;
align-items: center;
color: #ff6034;
font-weight: 600;
.rating-star {
font-size: 22px;
margin-right: 2px;
}
.rating-value {
font-size: 26px;
}
}
.sales {
color: #646566;
}
.delivery-time {
color: #1989fa;
}
.rating-sep {
margin: 0 8px;
color: #c8c9cc;
}
}
/* 起送 + 配送费 */
.shop-fee-row {
display: flex;
align-items: center;
margin-top: 8px;
font-size: 22px;
color: #969799;
.fee-sep {
margin: 0 6px;
color: #c8c9cc;
}
}
/* 标签 */
.shop-tags {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 10px;
.shop-tag {
margin: 0;
}
}
/* 商家介绍 */
.shop-desc {
margin-top: 8px;
font-size: 24px;
color: #969799;
line-height: 1.4;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
overflow: hidden;
}
+79
View File
@@ -0,0 +1,79 @@
import { View, Text } from '@tarojs/components'
import { Image as VantImage, Tag } from '@antmjs/vantui'
import type { Shop } from '@/types/shop'
import './index.less'
interface Props {
shop: Shop
onClick?: (shop: Shop) => void
}
export default function ShopCard({ shop, onClick }: Props) {
return (
<View className='shop-card' onClick={() => onClick?.(shop)}>
{/* 左侧:商家主图 */}
<View className='shop-image-wrap'>
<VantImage
className='shop-image'
src={shop.image}
fit='cover'
width='100%'
height='100%'
radius={8}
/>
</View>
{/* 右侧:商家信息 */}
<View className='shop-info'>
{/* 商家名 */}
<View className='shop-name-row'>
<Text className='shop-name'>{shop.name}</Text>
</View>
{/* 评分 + 月售 + 配送时间 */}
<View className='shop-rating-row'>
<View className='rating'>
<Text className='rating-star'></Text>
<Text className='rating-value'>{shop.rating.toFixed(1)}</Text>
</View>
<Text className='rating-sep'>·</Text>
<Text className='sales'> {shop.monthlySales}</Text>
<Text className='rating-sep'>·</Text>
<Text className='delivery-time'>{shop.deliveryTime}</Text>
</View>
{/* 起送 + 配送费 + 距离 */}
<View className='shop-fee-row'>
<Text className='fee'> ¥{shop.minOrder}</Text>
<Text className='fee-sep'>|</Text>
<Text className='fee'> ¥{shop.deliveryFee}</Text>
<Text className='fee-sep'>|</Text>
<Text className='distance'>{shop.distance}m</Text>
</View>
{/* 标签 */}
{shop.tags.length > 0 && (
<View className='shop-tags'>
{shop.tags.map((t) => {
const isPrimary = t === '招牌' || t === '推荐' || t === '热销' || t === '新店'
return (
<Tag
key={t}
type={isPrimary ? 'danger' : 'primary'}
plain
size='medium'
className='shop-tag'
>
{t}
</Tag>
)
})}
</View>
)}
{/* 商家介绍 */}
<Text className='shop-desc'>{shop.description}</Text>
</View>
</View>
)
}
+2 -2
View File
@@ -24,8 +24,8 @@ export const banners: BannerItem[] = [
/** 导航组数据 (2行 x 5列 = 10项) */
export const navItems: NavItem[] = [
{ id: '1', label: '课程表', icon: 'orders-o', path: '' },
{ id: '2', label: '通知公告', icon: 'bullhorn-o', path: '' },
{ id: '1', label: '校园外卖', icon: 'shop-o', path: '/pages/food-delivery/index' },
{ id: '2', label: '校园论坛', icon: 'comment-o', path: '/pages/forum/index' },
{ id: '3', label: '成绩查询', icon: 'chart-trending-o', path: '' },
{ id: '4', label: '作业提交', icon: 'edit', path: '' },
{ id: '5', label: '考试安排', icon: 'description', path: '' },
+343
View File
@@ -0,0 +1,343 @@
import type { Article, ForumSubCategory } from '@/types/article'
/** 论坛帖子(数据复用 Article 类型,category='forum'subCategory 区分板块) */
export const forumPosts: Article[] = [
// ===== 校园生活 =====
{
id: 'f1',
title: '学校南门新开了一家手打柠檬茶,超好喝!',
excerpt: '今天路过南门发现新开了一家柠檬茶店,买了一杯鸭屎香柠檬茶,8 块一杯,茶底是现煮的,柠檬也是现切,酸甜度刚好,强烈推荐给大家!',
source: '吃货小张',
sourceAvatar: 'https://picsum.photos/seed/av1/100/100',
time: '10 分钟前',
readCount: 234,
commentCount: 18,
likeCount: 56,
images: [
'https://picsum.photos/seed/lemon1/300/300',
'https://picsum.photos/seed/lemon2/300/300',
],
type: 'image-text',
category: 'forum',
subCategory: 'life',
},
{
id: 'f2',
title: '求问校运会的时间和报名方式',
excerpt: '请问今年校运会什么时候开始?在哪里报名呢?看学院群里发的通知好像错过了,有同学知道吗?',
source: '新生小李',
sourceAvatar: 'https://picsum.photos/seed/av2/100/100',
time: '1 小时前',
readCount: 89,
commentCount: 12,
likeCount: 8,
images: [],
type: 'text',
category: 'forum',
subCategory: 'life',
},
{
id: 'f3',
title: '分享一下我的毕业旅行 vlog 拍摄地',
excerpt: '毕业季到了,跟室友去了一趟川西,沿途的风景真的美哭了!把推荐的几个人少的拍摄点整理了一下,需要的学弟学妹可以参考。',
source: '应届毕业生',
sourceAvatar: 'https://picsum.photos/seed/av3/100/100',
time: '3 小时前',
readCount: 567,
commentCount: 45,
likeCount: 178,
images: [
'https://picsum.photos/seed/travel1/300/300',
'https://picsum.photos/seed/travel2/300/300',
'https://picsum.photos/seed/travel3/300/300',
],
type: 'image-grid',
category: 'forum',
subCategory: 'life',
},
{
id: 'f4',
title: '校园里的流浪猫越来越多了,建议学校组织救助',
excerpt: '每天去图书馆都能看到几只流浪猫,有的还很瘦。希望学校能联系相关公益组织,做一次集中的绝育和疫苗。',
source: '爱猫人士',
sourceAvatar: 'https://picsum.photos/seed/av4/100/100',
time: '昨天',
readCount: 412,
commentCount: 67,
likeCount: 234,
images: [],
type: 'text',
category: 'forum',
subCategory: 'life',
},
// ===== 学习交流 =====
{
id: 'f5',
title: '高等数学期末复习资料分享(含思维导图)',
excerpt: '整理了高数老师上课的全部重点,配上思维导图和典型例题,有需要的同学可以下载,祝大家期末都能 90+!',
source: '数学学霸',
sourceAvatar: 'https://picsum.photos/seed/av5/100/100',
time: '20 分钟前',
readCount: 1023,
commentCount: 89,
likeCount: 567,
images: [],
type: 'text',
category: 'forum',
subCategory: 'study',
},
{
id: 'f6',
title: '求推荐计算机专业研究生导师',
excerpt: '本人是计科大三学生,绩点 3.6,想考研但对导师方向不太了解。请问有没有熟悉图像处理和 CV 方向的老师推荐?',
source: '考研小白',
sourceAvatar: 'https://picsum.photos/seed/av6/100/100',
time: '2 小时前',
readCount: 256,
commentCount: 34,
likeCount: 28,
images: [],
type: 'text',
category: 'forum',
subCategory: 'study',
},
{
id: 'f7',
title: '英语六级备考经验贴,2 个月从 420 到 568',
excerpt: '我英语底子一般,经过 2 个月的系统复习,最终六级 568 分。把我的复习方法、资料和刷题技巧全部分享出来。',
source: '六级过儿',
sourceAvatar: 'https://picsum.photos/seed/av7/100/100',
time: '4 小时前',
readCount: 892,
commentCount: 56,
likeCount: 312,
images: [
'https://picsum.photos/seed/english1/300/300',
'https://picsum.photos/seed/english2/300/300',
],
type: 'image-text',
category: 'forum',
subCategory: 'study',
},
{
id: 'f8',
title: '高数线代答疑群,有问题来问',
excerpt: '我们几个同学建了一个数学答疑群,有不会的题目可以发出来互相讨论。群里 200 多人,气氛活跃,不收任何费用。',
source: '数学小组',
sourceAvatar: 'https://picsum.photos/seed/av8/100/100',
time: '2 天前',
readCount: 456,
commentCount: 23,
likeCount: 89,
images: [],
type: 'text',
category: 'forum',
subCategory: 'study',
},
// ===== 失物招领 =====
{
id: 'f9',
title: '【寻物】昨天图书馆丢失蓝色保温杯',
excerpt: '昨天上午在三楼自习室落下一个蓝色的 Stanley 保温杯,杯身贴了一张小熊贴纸。有同学看到请联系本人,必有重谢!',
source: '焦急的小王',
sourceAvatar: 'https://picsum.photos/seed/av9/100/100',
time: '30 分钟前',
readCount: 178,
commentCount: 14,
likeCount: 12,
images: [
'https://picsum.photos/seed/cup1/300/300',
],
type: 'image-text',
category: 'forum',
subCategory: 'lost-found',
},
{
id: 'f10',
title: '【招领】在操场捡到一串钥匙',
excerpt: '昨天晚上在西操场跑步时捡到一串钥匙,大概 3-4 把,带一个蓝色的小熊挂件。有丢失的同学可以私信我核对信息。',
source: '好心的同学',
sourceAvatar: 'https://picsum.photos/seed/av10/100/100',
time: '5 小时前',
readCount: 92,
commentCount: 8,
likeCount: 15,
images: [
'https://picsum.photos/seed/keys1/300/300',
],
type: 'image-text',
category: 'forum',
subCategory: 'lost-found',
},
{
id: 'f11',
title: '【寻物】一卡通不见了,应该在教学楼附近',
excerpt: '今天中午在第二教学楼附近丢了一张一卡通,名字是张三,学号 2021****。如果捡到请联系 138****1234,必有红包感谢!',
source: '张三同学',
sourceAvatar: 'https://picsum.photos/seed/av11/100/100',
time: '昨天',
readCount: 134,
commentCount: 11,
likeCount: 6,
images: [],
type: 'text',
category: 'forum',
subCategory: 'lost-found',
},
{
id: 'f12',
title: '【招领】食堂捡到一部黑色手机',
excerpt: '今天中午在第二食堂二楼捡到一部黑色 iPhone,已经交给食堂前台保管了。失主可以凭锁屏密码前往认领。',
source: '值班阿姨',
sourceAvatar: 'https://picsum.photos/seed/av12/100/100',
time: '2 天前',
readCount: 245,
commentCount: 19,
likeCount: 34,
images: [],
type: 'text',
category: 'forum',
subCategory: 'lost-found',
},
// ===== 二手交易 =====
{
id: 'f13',
title: '出 9 成新 iPad Air 564G 太空灰,附原装保护壳',
excerpt: '去年 9 月购入,平常只用来记笔记和看剧,9 成新无磕碰。原价 4799,现 3200 出,送原装保护壳和钢化膜,可面交验机。',
source: '毕业学姐',
sourceAvatar: 'https://picsum.photos/seed/av13/100/100',
time: '15 分钟前',
readCount: 678,
commentCount: 42,
likeCount: 89,
images: [
'https://picsum.photos/seed/ipad1/300/300',
'https://picsum.photos/seed/ipad2/300/300',
'https://picsum.photos/seed/ipad3/300/300',
],
type: 'image-grid',
category: 'forum',
subCategory: 'secondhand',
},
{
id: 'f14',
title: '便宜出考研政治英语资料一套',
excerpt: '今年考研上岸,低价出政治英语全套复习资料,包含肖四肖八、考研真相等,原价 500 多,现在 80 元打包出。',
source: '上岸学长',
sourceAvatar: 'https://picsum.photos/seed/av14/100/100',
time: '1 小时前',
readCount: 312,
commentCount: 26,
likeCount: 45,
images: [],
type: 'text',
category: 'forum',
subCategory: 'secondhand',
},
{
id: 'f15',
title: '出 9 成新小米手环 8,黑色',
excerpt: '用了 2 个月,9 成新,功能正常,配 2 根表带。原价 239,现 100 元,可小刀。',
source: '数码爱好者',
sourceAvatar: 'https://picsum.photos/seed/av15/100/100',
time: '3 小时前',
readCount: 89,
commentCount: 7,
likeCount: 12,
images: [
'https://picsum.photos/seed/band1/300/300',
],
type: 'image-text',
category: 'forum',
subCategory: 'secondhand',
},
{
id: 'f16',
title: '毕业出闲置:台灯、收纳盒、衣架、教材等',
excerpt: '马上要搬走了,处理一批闲置物品:台灯、桌面收纳盒、晾衣架、几本教材、瑜伽垫等,价格都很便宜,列表如图。',
source: '毕业生小陈',
sourceAvatar: 'https://picsum.photos/seed/av16/100/100',
time: '昨天',
readCount: 567,
commentCount: 31,
likeCount: 67,
images: [
'https://picsum.photos/seed/items1/300/300',
'https://picsum.photos/seed/items2/300/300',
],
type: 'image-text',
category: 'forum',
subCategory: 'secondhand',
},
// ===== 树洞 =====
{
id: 'f17',
title: '今天答辩终于过了,谢谢我的导师',
excerpt: '从开题到答辩,整整一年时间。中间崩溃过很多次,每次都想放弃。今天终于听到"同意授予学位"那句话,眼泪当场就下来了。',
source: '匿名',
sourceAvatar: 'https://picsum.photos/seed/av17/100/100',
time: '1 小时前',
readCount: 1245,
commentCount: 156,
likeCount: 678,
images: [],
type: 'text',
category: 'forum',
subCategory: 'tree-hole',
},
{
id: 'f18',
title: '大学四年最遗憾的事',
excerpt: '快毕业了,回想这四年最遗憾的事,应该是没有好好谈一场恋爱吧。每天都在图书馆和实验室之间两点一线,错过了很多。',
source: '匿名',
sourceAvatar: 'https://picsum.photos/seed/av18/100/100',
time: '5 小时前',
readCount: 2345,
commentCount: 234,
likeCount: 1234,
images: [],
type: 'text',
category: 'forum',
subCategory: 'tree-hole',
},
{
id: 'f19',
title: '谢谢那个在雨天给我撑伞的陌生人',
excerpt: '今天下午在图书馆门口突然下暴雨,我正发愁怎么回去,一个不认识的学姐默默走过来,把伞塞到我手里说"你先用",然后自己跑进了雨里。',
source: '温暖的小事',
sourceAvatar: 'https://picsum.photos/seed/av19/100/100',
time: '昨天',
readCount: 1567,
commentCount: 198,
likeCount: 892,
images: [],
type: 'text',
category: 'forum',
subCategory: 'tree-hole',
},
{
id: 'f20',
title: '家里出了点事,最近压力很大',
excerpt: '不想发朋友圈,怕爸妈看到担心。在这里发泄一下吧。最近真的太累了,每天晚上都睡不着,但又不敢跟同学说。',
source: '匿名',
sourceAvatar: 'https://picsum.photos/seed/av20/100/100',
time: '2 天前',
readCount: 789,
commentCount: 87,
likeCount: 345,
images: [],
type: 'text',
category: 'forum',
subCategory: 'tree-hole',
},
]
/** 按子分类过滤论坛帖子 */
export function getForumPostsBySubCategory(sub: ForumSubCategory | 'all'): Article[] {
if (sub === 'all') return forumPosts
return forumPosts.filter((p) => p.subCategory === sub)
}
+249
View File
@@ -0,0 +1,249 @@
import type { MenuItem, MenuCategory } from '@/types/shop'
/** 每个店铺的菜单分类(key=shopId */
export const shopMenuCategories: Record<string, MenuCategory[]> = {
s1: [
{ id: 's1-c1', name: '招牌推荐' },
{ id: 's1-c2', name: '荤菜' },
{ id: 's1-c3', name: '素菜' },
{ id: 's1-c4', name: '主食' },
],
s2: [
{ id: 's2-c1', name: '招牌' },
{ id: 's2-c2', name: '小份配菜' },
{ id: 's2-c3', name: '饮品' },
],
s3: [
{ id: 's3-c1', name: '生日蛋糕' },
{ id: 's3-c2', name: '切块蛋糕' },
{ id: 's3-c3', name: '面包西点' },
],
s4: [
{ id: 's4-c1', name: '招牌奶茶' },
{ id: 's4-c2', name: '纯茶' },
{ id: 's4-c3', name: '加料' },
],
s5: [
{ id: 's5-c1', name: '热销零食' },
{ id: 's5-c2', name: '饮料' },
{ id: 's5-c3', name: '速食' },
{ id: 's5-c4', name: '日用品' },
],
s6: [
{ id: 's6-c1', name: '感冒发烧' },
{ id: 's6-c2', name: '肠胃护理' },
{ id: 's6-c3', name: '外用药品' },
{ id: 's6-c4', name: '保健营养' },
],
s7: [
{ id: 's7-c1', name: '时令水果' },
{ id: 's7-c2', name: '水果切盒' },
{ id: 's7-c3', name: '果汁' },
],
s8: [
{ id: 's8-c1', name: '招牌套餐' },
{ id: 's8-c2', name: '汉堡' },
{ id: 's8-c3', name: '小食' },
{ id: 's8-c4', name: '饮料' },
],
}
/** 商家-菜单项映射(key=shopId */
export const shopMenus: Record<string, MenuItem[]> = {
// === 张亮麻辣烫 ===
s1: [
{
id: 's1-1', name: '招牌骨汤麻辣烫', image: 'https://picsum.photos/seed/m1/300/300',
price: 25, originalPrice: 30, sales: 892, description: '骨汤熬制 4 小时,配菜任选 8 种',
tags: ['招牌', '热销'], categoryId: 's1-c1',
},
{
id: 's1-2', name: '麻辣牛肉串', image: 'https://picsum.photos/seed/m2/300/300',
price: 8, sales: 521, description: '秘制腌制,麻辣鲜香', categoryId: 's1-c1',
},
{
id: 's1-3', name: '鲜虾', image: 'https://picsum.photos/seed/m3/300/300',
price: 6, sales: 312, description: '新鲜活虾,去虾线', categoryId: 's1-c2',
},
{
id: 's1-4', name: '牛百叶', image: 'https://picsum.photos/seed/m4/300/300',
price: 12, sales: 256, description: '脆嫩爽口', categoryId: 's1-c2',
},
{
id: 's1-5', name: '土豆片', image: 'https://picsum.photos/seed/m5/300/300',
price: 4, sales: 845, description: '薄切易入味', categoryId: 's1-c3',
},
{
id: 's1-6', name: '金针菇', image: 'https://picsum.photos/seed/m6/300/300',
price: 5, sales: 612, description: '细嫩爽滑', categoryId: 's1-c3',
},
{
id: 's1-7', name: '白米饭', image: 'https://picsum.photos/seed/m7/300/300',
price: 2, sales: 1023, description: '东北珍珠米', categoryId: 's1-c4',
},
],
// === 黄焖鸡米饭 ===
s2: [
{
id: 's2-1', name: '黄焖鸡腿肉(小份)', image: 'https://picsum.photos/seed/m8/300/300',
price: 18, originalPrice: 22, sales: 723, description: '鸡腿肉 + 香菇 + 青椒,米饭另算',
tags: ['招牌'], categoryId: 's2-c1',
},
{
id: 's2-2', name: '黄焖排骨', image: 'https://picsum.photos/seed/m9/300/300',
price: 22, sales: 421, description: '猪肋排慢炖', categoryId: 's2-c1',
},
{
id: 's2-3', name: '香辣豆腐', image: 'https://picsum.photos/seed/m10/300/300',
price: 6, sales: 312, description: '嫩豆腐配辣椒', categoryId: 's2-c2',
},
{
id: 's2-4', name: '凉拌黄瓜', image: 'https://picsum.photos/seed/m11/300/300',
price: 5, sales: 245, description: '清爽开胃', categoryId: 's2-c2',
},
{
id: 's2-5', name: '酸梅汤', image: 'https://picsum.photos/seed/m12/300/300',
price: 5, sales: 568, description: '自制酸梅汤', categoryId: 's2-c3',
},
],
// === 幸福西饼 ===
s3: [
{
id: 's3-1', name: '榴莲千层蛋糕(6寸)', image: 'https://picsum.photos/seed/m13/300/300',
price: 138, sales: 245, description: '纯动物奶油 + 泰国金枕榴莲',
tags: ['招牌'], categoryId: 's3-c1',
},
{
id: 's3-2', name: '草莓鲜奶蛋糕(8寸)', image: 'https://picsum.photos/seed/m14/300/300',
price: 168, sales: 189, description: '新鲜草莓 + 动物奶油', categoryId: 's3-c1',
},
{
id: 's3-3', name: '巧克力切块', image: 'https://picsum.photos/seed/m15/300/300',
price: 18, sales: 567, description: '70% 黑巧', categoryId: 's3-c2',
},
{
id: 's3-4', name: '葡式蛋挞(4 个)', image: 'https://picsum.photos/seed/m16/300/300',
price: 16, sales: 821, description: '酥皮层层', categoryId: 's3-c3',
},
],
// === 一点点奶茶 ===
s4: [
{
id: 's4-1', name: '波霸奶茶(中杯)', image: 'https://picsum.photos/seed/m17/300/300',
price: 12, sales: 1253, description: '招牌波霸,茶味浓郁', tags: ['招牌', '热销'],
categoryId: 's4-c1',
},
{
id: 's4-2', name: '四季春玛奇朵', image: 'https://picsum.photos/seed/m18/300/300',
price: 14, sales: 856, description: '清茶 + 奶盖', categoryId: 's4-c1',
},
{
id: 's4-3', name: '茉莉绿妍', image: 'https://picsum.photos/seed/m19/300/300',
price: 8, sales: 421, description: '无糖纯茶', categoryId: 's4-c2',
},
{
id: 's4-4', name: '波霸', image: 'https://picsum.photos/seed/m20/300/300',
price: 2, sales: 1456, description: 'Q 弹有嚼劲', categoryId: 's4-c3',
},
],
// === 校园便利超市 ===
s5: [
{
id: 's5-1', name: '乐事薯片(大)', image: 'https://picsum.photos/seed/m21/300/300',
price: 8, sales: 562, description: '原味/黄瓜味', tags: ['热销'], categoryId: 's5-c1',
},
{
id: 's5-2', name: '奥利奥饼干', image: 'https://picsum.photos/seed/m22/300/300',
price: 6, sales: 423, description: '经典原味', categoryId: 's5-c1',
},
{
id: 's5-3', name: '可口可乐(330ml', image: 'https://picsum.photos/seed/m23/300/300',
price: 3, sales: 892, description: '冰镇更爽', categoryId: 's5-c2',
},
{
id: 's5-4', name: '康师傅红烧牛肉面', image: 'https://picsum.photos/seed/m24/300/300',
price: 4, sales: 1256, description: '宿舍夜宵必备', tags: ['热销'], categoryId: 's5-c3',
},
{
id: 's5-5', name: '抽纸 3 层 100 抽', image: 'https://picsum.photos/seed/m25/300/300',
price: 12, sales: 234, description: '宿舍装', categoryId: 's5-c4',
},
],
// === 老百姓大药房 ===
s6: [
{
id: 's6-1', name: '感冒灵颗粒(10 袋)', image: 'https://picsum.photos/seed/m26/300/300',
price: 15, sales: 312, description: '风寒感冒', tags: ['热销'], categoryId: 's6-c1',
},
{
id: 's6-2', name: '布洛芬缓释胶囊', image: 'https://picsum.photos/seed/m27/300/300',
price: 18, sales: 245, description: '退烧止痛', categoryId: 's6-c1',
},
{
id: 's6-3', name: '蒙脱石散', image: 'https://picsum.photos/seed/m28/300/300',
price: 22, sales: 156, description: '止泻', categoryId: 's6-c2',
},
{
id: 's6-4', name: '云南白药创可贴', image: 'https://picsum.photos/seed/m29/300/300',
price: 8, sales: 421, description: '100 片装', categoryId: 's6-c3',
},
{
id: 's6-5', name: '维生素 C 片', image: 'https://picsum.photos/seed/m30/300/300',
price: 25, sales: 189, description: '100 片装', categoryId: 's6-c4',
},
],
// === 鲜果时光 ===
s7: [
{
id: 's7-1', name: '海南金煌芒(1 个)', image: 'https://picsum.photos/seed/m31/300/300',
price: 12, sales: 567, description: '香甜无纤维', tags: ['招牌'], categoryId: 's7-c1',
},
{
id: 's7-2', name: '泰国山竹(5 个)', image: 'https://picsum.photos/seed/m32/300/300',
price: 28, sales: 245, description: '当季新鲜', categoryId: 's7-c1',
},
{
id: 's7-3', name: '水果切盒(大)', image: 'https://picsum.photos/seed/m33/300/300',
price: 25, sales: 892, description: '西瓜+哈密瓜+火龙果+芒果', tags: ['热销'],
categoryId: 's7-c2',
},
{
id: 's7-4', name: '鲜榨橙汁', image: 'https://picsum.photos/seed/m34/300/300',
price: 15, sales: 412, description: '现榨无添加', categoryId: 's7-c3',
},
],
// === 肯德基 ===
s8: [
{
id: 's8-1', name: '香辣鸡腿堡套餐', image: 'https://picsum.photos/seed/m35/300/300',
price: 32, originalPrice: 38, sales: 856, description: '汉堡 + 中薯 + 中可乐',
tags: ['招牌', '套餐'], categoryId: 's8-c1',
},
{
id: 's8-2', name: '黄金鸡块(5 块)', image: 'https://picsum.photos/seed/m36/300/300',
price: 15, sales: 632, description: '外酥里嫩', categoryId: 's8-c1',
},
{
id: 's8-3', name: '劲脆鸡腿堡', image: 'https://picsum.photos/seed/m37/300/300',
price: 18, sales: 723, description: '全明星单品', tags: ['热销'], categoryId: 's8-c2',
},
{
id: 's8-4', name: '薯条(中)', image: 'https://picsum.photos/seed/m38/300/300',
price: 12, sales: 945, description: '金黄酥脆', categoryId: 's8-c3',
},
{
id: 's8-5', name: '百事可乐(中)', image: 'https://picsum.photos/seed/m39/300/300',
price: 8, sales: 756, description: '冰爽解腻', categoryId: 's8-c4',
},
],
}
/** 获取某店铺的菜单分类 */
export function getMenuCategories(shopId: string): MenuCategory[] {
return shopMenuCategories[shopId] || []
}
/** 获取某店铺的菜单项(按分类分组) */
export function getMenuItems(shopId: string): MenuItem[] {
return shopMenus[shopId] || []
}
+111
View File
@@ -0,0 +1,111 @@
import type { Review } from '@/types/shop'
/** 商家-评价映射(key=shopId */
export const shopReviews: Record<string, Review[]> = {
s1: [
{
id: 'r1', author: { name: '小王同学', avatar: 'https://picsum.photos/seed/u1/100/100' },
rating: 5, time: '2 小时前', content: '味道正宗,骨汤浓郁,配送也很快,包装完好,下次还会再点!',
images: [
'https://picsum.photos/seed/rv1/200/200',
'https://picsum.photos/seed/rv2/200/200',
],
dishes: ['招牌骨汤麻辣烫', '麻辣牛肉串'],
},
{
id: 'r2', author: { name: '匿名用户', avatar: 'https://picsum.photos/seed/u2/100/100' },
rating: 4, time: '昨天', content: '份量挺足的,价格也合理,麻酱香味很浓。',
dishes: ['牛百叶', '土豆片'],
},
{
id: 'r3', author: { name: '李同学', avatar: 'https://picsum.photos/seed/u3/100/100' },
rating: 5, time: '3 天前', content: '室友推荐的,确实不错,辣度可以自由选。',
},
],
s2: [
{
id: 'r4', author: { name: '王同学', avatar: 'https://picsum.photos/seed/u4/100/100' },
rating: 5, time: '1 小时前', content: '鸡腿肉很嫩,酱香浓郁,米饭再加一份才过瘾。',
images: ['https://picsum.photos/seed/rv3/200/200'],
dishes: ['黄焖鸡腿肉(小份)'],
},
{
id: 'r5', author: { name: '张同学', avatar: 'https://picsum.photos/seed/u5/100/100' },
rating: 4, time: '昨天', content: '份量足,送达也快,酸梅汤也好喝。',
},
],
s3: [
{
id: 'r6', author: { name: '陈同学', avatar: 'https://picsum.photos/seed/u6/100/100' },
rating: 5, time: '30 分钟前', content: '朋友过生日订的,榴莲千层真的绝绝子,奶油不腻!',
images: [
'https://picsum.photos/seed/rv4/200/200',
'https://picsum.photos/seed/rv5/200/200',
'https://picsum.photos/seed/rv6/200/200',
],
dishes: ['榴莲千层蛋糕(6寸)'],
},
{
id: 'r7', author: { name: '刘同学', avatar: 'https://picsum.photos/seed/u7/100/100' },
rating: 5, time: '2 天前', content: '蛋挞很香,酥皮层次分明。',
dishes: ['葡式蛋挞(4 个)'],
},
],
s4: [
{
id: 'r8', author: { name: '周同学', avatar: 'https://picsum.photos/seed/u8/100/100' },
rating: 5, time: '10 分钟前', content: '波霸 Q 弹,茶底清淡,甜度可以自由选太赞了。',
dishes: ['波霸奶茶(中杯)'],
},
{
id: 'r9', author: { name: '吴同学', avatar: 'https://picsum.photos/seed/u9/100/100' },
rating: 4, time: '5 小时前', content: '送货快,包装也结实,奶盖略甜。',
},
],
s5: [
{
id: 'r10', author: { name: '钱同学', avatar: 'https://picsum.photos/seed/u10/100/100' },
rating: 5, time: '20 分钟前', content: '宿舍楼下的便利店太方便了,深夜还能送泡面。',
images: ['https://picsum.photos/seed/rv7/200/200'],
dishes: ['康师傅红烧牛肉面', '可口可乐(330ml'],
},
{
id: 'r11', author: { name: '孙同学', avatar: 'https://picsum.photos/seed/u11/100/100' },
rating: 4, time: '昨天', content: '送货快,价格合理,商品全。',
},
],
s6: [
{
id: 'r12', author: { name: '郑同学', avatar: 'https://picsum.photos/seed/u12/100/100' },
rating: 5, time: '1 小时前', content: '半夜发烧送的药,骑手很负责,还特别交代了服用方法。',
dishes: ['布洛芬缓释胶囊'],
},
],
s7: [
{
id: 'r13', author: { name: '冯同学', avatar: 'https://picsum.photos/seed/u13/100/100' },
rating: 5, time: '15 分钟前', content: '水果很新鲜,芒果超甜,切盒也很新鲜。',
images: [
'https://picsum.photos/seed/rv8/200/200',
'https://picsum.photos/seed/rv9/200/200',
],
dishes: ['水果切盒(大)', '海南金煌芒(1 个)'],
},
],
s8: [
{
id: 'r14', author: { name: '蒋同学', avatar: 'https://picsum.photos/seed/u14/100/100' },
rating: 5, time: '40 分钟前', content: '校园店出餐快,套餐价格比店里便宜。',
dishes: ['香辣鸡腿堡套餐'],
},
{
id: 'r15', author: { name: '魏同学', avatar: 'https://picsum.photos/seed/u15/100/100' },
rating: 4, time: '昨天', content: '薯条有点凉了,不过整体还可以。',
},
],
}
/** 获取某店铺的评价列表 */
export function getShopReviews(shopId: string): Review[] {
return shopReviews[shopId] || []
}
+222
View File
@@ -0,0 +1,222 @@
import type { Shop, ShopCategory } from '@/types/shop'
/** 商家列表 */
export const shops: Shop[] = [
{
id: 's1',
name: '张亮麻辣烫',
image: 'https://picsum.photos/seed/shop1/400/400',
logo: 'https://picsum.photos/seed/s1logo/120/120',
rating: 4.8,
monthlySales: 3280,
deliveryTime: 28,
deliveryFee: 3,
minOrder: 20,
distance: '320',
category: 'food',
tags: ['满减', '招牌', '推荐'],
description: '正宗骨汤熬制,食材新鲜,多种自选菜品,搭配麻酱一口香',
recommended: true,
address: '南门外商业街 B 座 12 号',
phone: '0571-8888-0001',
businessHours: '10:00-22:00',
deliveryHours: '立即配送',
services: ['可堂食', '支持开发票', '提供 WiFi'],
photos: [
'https://picsum.photos/seed/s1p1/600/400',
'https://picsum.photos/seed/s1p2/600/400',
'https://picsum.photos/seed/s1p3/600/400',
],
announcements: '满 30 减 5,满 50 减 10,配送费立减 1 元',
},
{
id: 's2',
name: '黄焖鸡米饭',
image: 'https://picsum.photos/seed/shop2/400/400',
logo: 'https://picsum.photos/seed/s2logo/120/120',
rating: 4.6,
monthlySales: 2156,
deliveryTime: 25,
deliveryFee: 2,
minOrder: 15,
distance: '180',
category: 'food',
tags: ['爆款', '推荐'],
description: '秘制酱料焖制鸡腿肉,配米饭一绝,分量足、味道正',
recommended: true,
address: '东门小吃街 8 号',
phone: '0571-8888-0002',
businessHours: '09:30-21:30',
deliveryHours: '立即配送',
services: ['可堂食', '校园直送'],
photos: [
'https://picsum.photos/seed/s2p1/600/400',
'https://picsum.photos/seed/s2p2/600/400',
],
},
{
id: 's3',
name: '幸福西饼',
image: 'https://picsum.photos/seed/shop3/400/400',
logo: 'https://picsum.photos/seed/s3logo/120/120',
rating: 4.9,
monthlySales: 1567,
deliveryTime: 35,
deliveryFee: 5,
minOrder: 30,
distance: '650',
category: 'dessert',
tags: ['新店', '推荐'],
description: '动物奶油新鲜现做,2 公里免费配送,生日蛋糕提前 6 小时预订',
recommended: true,
address: '校园中央商业广场 2 楼',
phone: '0571-8888-0003',
businessHours: '08:00-22:00',
deliveryHours: '立即配送',
services: ['支持开发票', '生日蛋糕预订', '免费配送'],
photos: [
'https://picsum.photos/seed/s3p1/600/400',
'https://picsum.photos/seed/s3p2/600/400',
'https://picsum.photos/seed/s3p3/600/400',
],
announcements: '新人首单立减 10 元',
},
{
id: 's4',
name: '一点点奶茶',
image: 'https://picsum.photos/seed/shop4/400/400',
logo: 'https://picsum.photos/seed/s4logo/120/120',
rating: 4.7,
monthlySales: 4521,
deliveryTime: 22,
deliveryFee: 3,
minOrder: 18,
distance: '90',
category: 'dessert',
tags: ['满减', '热销'],
description: '现煮茶底,可加波霸/椰果/布丁,甜度冰度自由选',
address: '西门外大学路 23 号',
phone: '0571-8888-0004',
businessHours: '09:00-23:00',
deliveryHours: '立即配送',
services: ['可堂食', '校园直送'],
photos: [
'https://picsum.photos/seed/s4p1/600/400',
'https://picsum.photos/seed/s4p2/600/400',
],
},
{
id: 's5',
name: '校园便利超市',
image: 'https://picsum.photos/seed/shop5/400/400',
logo: 'https://picsum.photos/seed/s5logo/120/120',
rating: 4.5,
monthlySales: 5842,
deliveryTime: 18,
deliveryFee: 1,
minOrder: 10,
distance: '50',
category: 'supermarket',
tags: ['满减', '24小时'],
description: '零食、饮料、日用品、文具全覆盖,宿舍楼下送达',
address: '宿舍区 3 号楼 1 层',
phone: '0571-8888-0005',
businessHours: '00:00-24:00',
deliveryHours: '立即配送',
services: ['24 小时营业', '校园直送', '货到付款'],
photos: [
'https://picsum.photos/seed/s5p1/600/400',
'https://picsum.photos/seed/s5p2/600/400',
],
},
{
id: 's6',
name: '老百姓大药房',
image: 'https://picsum.photos/seed/shop6/400/400',
logo: 'https://picsum.photos/seed/s6logo/120/120',
rating: 4.8,
monthlySales: 892,
deliveryTime: 30,
deliveryFee: 4,
minOrder: 25,
distance: '420',
category: 'medicine',
tags: ['医保', '推荐'],
description: '常用药、感冒药、肠胃药、消暑药一应俱全,药师在线咨询',
address: '校医院旁 6 号',
phone: '0571-8888-0006',
businessHours: '08:00-22:00',
deliveryHours: '立即配送',
services: ['医保支付', '药师咨询', '24 小时送药'],
photos: [
'https://picsum.photos/seed/s6p1/600/400',
'https://picsum.photos/seed/s6p2/600/400',
],
announcements: '24 小时送药服务,夜班不间断',
},
{
id: 's7',
name: '鲜果时光',
image: 'https://picsum.photos/seed/shop7/400/400',
logo: 'https://picsum.photos/seed/s7logo/120/120',
rating: 4.7,
monthlySales: 1245,
deliveryTime: 25,
deliveryFee: 2,
minOrder: 20,
distance: '210',
category: 'fruit',
tags: ['新鲜', '满减'],
description: '应季水果直采,产地直发,水果切盒下午茶首选',
address: '南门水果街 5 号',
phone: '0571-8888-0007',
businessHours: '07:00-22:00',
deliveryHours: '立即配送',
services: ['产地直采', '坏果包赔'],
photos: [
'https://picsum.photos/seed/s7p1/600/400',
'https://picsum.photos/seed/s7p2/600/400',
],
},
{
id: 's8',
name: '肯德基(校园店)',
image: 'https://picsum.photos/seed/shop8/400/400',
logo: 'https://picsum.photos/seed/s8logo/120/120',
rating: 4.6,
monthlySales: 3210,
deliveryTime: 32,
deliveryFee: 5,
minOrder: 30,
distance: '480',
category: 'food',
tags: ['连锁', '品牌'],
description: '原味鸡、薯条、汉堡、可乐全套,校园专享优惠套餐',
address: '中央食堂 1 楼',
phone: '0571-8888-0008',
businessHours: '07:00-23:00',
deliveryHours: '立即配送',
services: ['可堂食', '品牌连锁', '支持开发票'],
photos: [
'https://picsum.photos/seed/s8p1/600/400',
'https://picsum.photos/seed/s8p2/600/400',
],
},
]
/** 根据 id 获取商家 */
export function getShopById(id: string): Shop | undefined {
return shops.find((s) => s.id === id)
}
/** 首页推荐商家 */
export function getRecommendedShops(limit?: number): Shop[] {
const list = shops.filter((s) => s.recommended)
return limit ? list.slice(0, limit) : list
}
/** 按分类筛选商家 */
export function getShopsByCategory(category: ShopCategory | 'all'): Shop[] {
if (category === 'all') return shops
return shops.filter((s) => s.category === category)
}
+27 -4
View File
@@ -306,11 +306,34 @@
}
/* 文章不存在 */
.empty-article {
.empty-article-wrap {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
font-size: 30px;
color: #969799;
padding: 200px 32px 0;
.empty-article-icon {
font-size: 80px;
margin-bottom: 24px;
opacity: 0.4;
}
.empty-article-text {
font-size: 30px;
color: #969799;
margin-bottom: 32px;
}
.empty-article-back-btn {
padding: 16px 48px;
background: #1989fa;
border-radius: 40px;
}
.empty-article-back-text {
font-size: 28px;
color: #fff;
font-weight: 500;
}
}
+21 -1
View File
@@ -64,7 +64,27 @@ export default function ArticleDetail() {
if (!article) {
return (
<View className='article-page'>
<View className='empty-article'></View>
{/* ========== 自定义导航栏(带返回) ========== */}
<View className='article-nav' style={{ paddingTop: `${statusBarH}px` }}>
<View className='nav-content'>
<View className='nav-back' onClick={handleBack}>
<Icon name='arrow-left' size={22} color='#323233' />
</View>
<Text className='nav-title'></Text>
<View className='nav-placeholder' />
</View>
</View>
{/* 占位 */}
<View style={{ height: `${statusBarH + 44}px` }} />
<View className='empty-article-wrap'>
<Text className='empty-article-icon'>📄</Text>
<Text className='empty-article-text'></Text>
<View className='empty-article-back-btn' onClick={handleBack}>
<Text className='empty-article-back-text'></Text>
</View>
</View>
</View>
)
}
@@ -1,4 +1,4 @@
export default definePageConfig({
navigationBarTitleText: '商品详情',
navigationStyle: 'custom',
navigationBarTitleText: '校园外卖',
})
+92
View File
@@ -0,0 +1,92 @@
/* ========================================
校园外卖首页
======================================== */
.food-delivery-page {
min-height: 100vh;
background: #f7f8fa;
display: flex;
flex-direction: column;
}
/* ========== 自定义导航栏(仅标题) ========== */
.custom-navbar {
background: #fff;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.04);
}
.navbar-content {
display: flex;
align-items: center;
justify-content: center;
padding: 0 24px;
}
.navbar-title-wrap {
display: flex;
align-items: center;
justify-content: center;
}
.navbar-title {
font-size: 34px;
font-weight: 600;
color: #1a1a1a;
}
/* ========== 主体滚动区域 ========== */
.page-scroll {
flex: 1;
}
/* ========== 主体内容区内的搜索栏 ========== */
.inline-search-section {
padding: 20px 24px 8px;
background: #fff;
}
/* ========== 顶部 5 个分类导航 ========== */
.category-section {
margin: 0 24px 12px;
background: #fff;
border-radius: 16px;
padding: 20px 8px 8px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
}
/* ========== 列表头 ========== */
.shop-list-header {
padding: 16px 24px 8px;
.shop-list-title {
font-size: 28px;
font-weight: 600;
color: #1a1a1a;
.shop-list-count {
font-size: 24px;
font-weight: 400;
color: #969799;
}
}
}
/* ========== 商家列表 ========== */
.shop-list {
margin: 0 24px;
background: #fff;
border-radius: 16px;
overflow: hidden;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
}
.empty-tip {
text-align: center;
padding: 80px 0;
font-size: 28px;
color: #c8c9cc;
}
.bottom-safe {
height: 40px;
}
+120
View File
@@ -0,0 +1,120 @@
import { useState, useMemo } from 'react'
import Taro from '@tarojs/taro'
import { View, Text, ScrollView } from '@tarojs/components'
import { Search, Grid, GridItem } from '@antmjs/vantui'
import ShopCard from '@/components/ShopCard'
import { shops } from '@/mock/shops'
import type { Shop, ShopCategory } from '@/types/shop'
import './index.less'
/** 获取状态栏 + 导航栏高度 */
function getNavBarHeight() {
const systemInfo = Taro.getSystemInfoSync()
const statusBarHeight = systemInfo.statusBarHeight || 20
const navBarHeight = 44
return { statusBarHeight, navBarHeight, totalHeight: statusBarHeight + navBarHeight }
}
/** 顶部 5 个分类导航 */
const CATEGORY_NAV: { id: ShopCategory; label: string; icon: string }[] = [
{ id: 'food', label: '美食', icon: '🍱' },
{ id: 'dessert', label: '甜点', icon: '🍰' },
{ id: 'supermarket', label: '超市便利', icon: '🛒' },
{ id: 'medicine', label: '买药', icon: '💊' },
{ id: 'fruit', label: '水果', icon: '🍎' },
]
export default function FoodDelivery() {
const [activeCategory, setActiveCategory] = useState<ShopCategory>('food')
const navBarHeight = useMemo(() => getNavBarHeight(), [])
/** 按当前分类过滤商家 */
const filteredShops = useMemo(
() => shops.filter((s) => s.category === activeCategory),
[activeCategory],
)
/** 搜索 */
const handleSearch = () => {
Taro.navigateTo({ url: '/pages/search/index' })
}
/** 点击商家 */
const handleShopClick = (shop: Shop) => {
Taro.navigateTo({ url: `/pages/shop-detail/index?id=${shop.id}` })
}
return (
<View className='food-delivery-page'>
{/* ========== 自定义导航栏(仅标题) ========== */}
<View
className='custom-navbar'
style={{ paddingTop: `${navBarHeight.statusBarHeight}px` }}
>
<View
className='navbar-content'
style={{ height: `${navBarHeight.navBarHeight}px` }}
>
<View className='navbar-title-wrap'>
<Text className='navbar-title'></Text>
</View>
</View>
</View>
{/* 占位 */}
<View style={{ height: `${navBarHeight.totalHeight}px` }} />
{/* ========== 主体内容(搜索 + 分类 + 列表)全部可滚动 ========== */}
<ScrollView
className='page-scroll'
scrollY
enhanced
showScrollbar={false}
>
{/* 搜索栏(主体内容区顶部) */}
<View className='inline-search-section'>
<Search
placeholder='搜索商家、菜品'
onClickInput={handleSearch}
shape='round'
background='#f2f3f5'
readonly
/>
</View>
{/* 5 个分类导航 */}
<View className='category-section'>
<Grid columnNum={5} border={false} gutter={4}>
{CATEGORY_NAV.map((cat) => (
<GridItem
key={cat.id}
icon={cat.icon}
text={cat.label}
onClick={() => setActiveCategory(cat.id)}
/>
))}
</Grid>
</View>
{/* 列表头 */}
<View className='shop-list-header'>
<Text className='shop-list-title'>
{CATEGORY_NAV.find((c) => c.id === activeCategory)?.label || ''} ·{' '}
<Text className='shop-list-count'>{filteredShops.length} </Text>
</Text>
</View>
{/* 商家列表 */}
<View className='shop-list'>
{filteredShops.map((shop) => (
<ShopCard key={shop.id} shop={shop} onClick={handleShopClick} />
))}
{filteredShops.length === 0 && (
<View className='empty-tip'></View>
)}
</View>
<View className='bottom-safe' />
</ScrollView>
</View>
)
}
+4
View File
@@ -0,0 +1,4 @@
export default definePageConfig({
navigationStyle: 'custom',
navigationBarTitleText: '校园论坛',
})
+115
View File
@@ -0,0 +1,115 @@
/* ========================================
校园论坛首页
======================================== */
.forum-page {
min-height: 100vh;
background: #f7f8fa;
display: flex;
flex-direction: column;
}
/* ========== 自定义导航栏(标题 + 发布) ========== */
.custom-navbar {
background: #fff;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.04);
}
.navbar-content {
display: flex;
align-items: center;
padding: 0 16px;
position: relative;
}
.navbar-title-wrap {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
}
.navbar-title {
font-size: 34px;
font-weight: 600;
color: #1a1a1a;
}
.navbar-publish {
position: absolute;
right: 16px;
top: 50%;
transform: translateY(-50%);
width: 60px;
height: 60px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background: #f7f8fa;
flex-shrink: 0;
&:active {
opacity: 0.7;
}
.publish-icon {
font-size: 30px;
}
}
/* ========== 主体滚动区域 ========== */
.page-scroll {
flex: 1;
}
/* ========== 主体内容区内的搜索栏 ========== */
.inline-search-section {
padding: 20px 24px 8px;
background: #fff;
}
/* ========== 顶部 5 个子分类导航 ========== */
.category-section {
margin: 0 24px 12px;
background: #fff;
border-radius: 16px;
padding: 24px 8px 16px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
}
/* ========== 列表头 ========== */
.post-list-header {
padding: 16px 24px 12px;
.post-list-title {
font-size: 28px;
font-weight: 600;
color: #1a1a1a;
.post-list-count {
font-size: 24px;
font-weight: 400;
color: #969799;
}
}
}
/* ========== 帖子列表(独立卡片,16px 间距) ========== */
.post-list {
display: flex;
flex-direction: column;
gap: 20px;
padding: 0 24px;
}
.empty-tip {
text-align: center;
padding: 80px 0;
font-size: 28px;
color: #c8c9cc;
}
.bottom-safe {
height: 40px;
}
+132
View File
@@ -0,0 +1,132 @@
import { useState, useMemo } from 'react'
import Taro from '@tarojs/taro'
import { View, Text, ScrollView } from '@tarojs/components'
import { Search, Grid, GridItem } from '@antmjs/vantui'
import ForumPostCard from '@/components/ForumPostCard'
import { getForumPostsBySubCategory } from '@/mock/forum'
import type { Article, ForumSubCategory } from '@/types/article'
import './index.less'
/** 获取状态栏 + 导航栏高度 */
function getNavBarHeight() {
const systemInfo = Taro.getSystemInfoSync()
const statusBarHeight = systemInfo.statusBarHeight || 20
const navBarHeight = 44
return { statusBarHeight, navBarHeight, totalHeight: statusBarHeight + navBarHeight }
}
/** 论坛 5 个子分类 */
const FORUM_CATEGORIES: { id: ForumSubCategory; label: string; icon: string }[] = [
{ id: 'life', label: '校园生活', icon: '🌸' },
{ id: 'study', label: '学习交流', icon: '📚' },
{ id: 'lost-found', label: '失物招领', icon: '🔍' },
{ id: 'secondhand', label: '二手交易', icon: '🛍' },
{ id: 'tree-hole', label: '树洞', icon: '🌳' },
]
export default function Forum() {
const [activeCategory, setActiveCategory] = useState<ForumSubCategory>('life')
const navBarHeight = useMemo(() => getNavBarHeight(), [])
/** 按当前子分类过滤帖子 */
const filteredPosts = useMemo(
() => getForumPostsBySubCategory(activeCategory),
[activeCategory],
)
/** 搜索 */
const handleSearch = () => {
Taro.navigateTo({ url: '/pages/search/index' })
}
/** 帖子点击 */
const handlePostClick = (post: Article) => {
Taro.navigateTo({ url: `/pages/article/index?id=${post.id}` })
}
/** 发布帖子 */
const handlePublish = () => {
Taro.showToast({ title: '发布功能即将上线', icon: 'none' })
}
return (
<View className='forum-page'>
{/* ========== 自定义导航栏(标题 + 发布) ========== */}
<View
className='custom-navbar'
style={{ paddingTop: `${navBarHeight.statusBarHeight}px` }}
>
<View
className='navbar-content'
style={{ height: `${navBarHeight.navBarHeight}px` }}
>
<View className='navbar-title-wrap'>
<Text className='navbar-title'></Text>
</View>
<View className='navbar-publish' onClick={handlePublish}>
<Text className='publish-icon'></Text>
</View>
</View>
</View>
{/* 占位 */}
<View style={{ height: `${navBarHeight.totalHeight}px` }} />
{/* ========== 主体内容(搜索 + 分类 + 列表)全部可滚动 ========== */}
<ScrollView
className='page-scroll'
scrollY
enhanced
showScrollbar={false}
>
{/* 搜索栏(主体内容区顶部) */}
<View className='inline-search-section'>
<Search
placeholder='搜索帖子、用户'
onClickInput={handleSearch}
shape='round'
background='#f2f3f5'
readonly
/>
</View>
{/* 5 个子分类导航 */}
<View className='category-section'>
<Grid columnNum={5} border={false} gutter={4}>
{FORUM_CATEGORIES.map((cat) => (
<GridItem
key={cat.id}
icon={cat.icon}
text={cat.label}
onClick={() => setActiveCategory(cat.id)}
/>
))}
</Grid>
</View>
{/* 列表头 */}
<View className='post-list-header'>
<Text className='post-list-title'>
{FORUM_CATEGORIES.find((c) => c.id === activeCategory)?.label || ''} ·{' '}
<Text className='post-list-count'>{filteredPosts.length} </Text>
</Text>
</View>
{/* 帖子列表 */}
<View className='post-list'>
{filteredPosts.map((post) => (
<ForumPostCard
key={post.id}
post={post}
onClick={handlePostClick}
/>
))}
{filteredPosts.length === 0 && (
<View className='empty-tip'></View>
)}
</View>
<View className='bottom-safe' />
</ScrollView>
</View>
)
}
+61 -78
View File
@@ -80,8 +80,8 @@
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
}
/* ========== 统一板块 ========== */
.main-tabs-section {
/* ========== 推荐商家 section ========== */
.recommended-shops-section {
margin: 0 24px 20px;
background: #fff;
border-radius: 16px;
@@ -89,92 +89,72 @@
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
}
/* 校园商城内容 */
.mall-content {
padding: 16px 20px 24px;
}
/* 分类标签 */
.category-scroll {
margin-bottom: 20px;
}
.category-tabs {
display: flex;
gap: 12px;
white-space: nowrap;
.category-tab {
flex-shrink: 0;
padding: 10px 24px;
border-radius: 32px;
background: #f7f8fa;
font-size: 26px;
color: #646566;
transition: all 0.2s;
&.active {
background: #1989fa;
color: #fff;
font-weight: 500;
}
}
}
/* 商品双列网格 */
.product-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
margin-bottom: 20px;
.empty-grid {
grid-column: 1 / -1;
text-align: center;
padding: 40px 0;
font-size: 28px;
color: #c8c9cc;
}
}
/* 商家入驻入口 */
.merchant-join-card {
.section-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20px 24px;
background: linear-gradient(135deg, #f0f7ff 0%, #e8f4ff 100%);
border: 1px dashed #1989fa;
border-radius: 12px;
padding: 24px 24px 12px;
.join-content {
.section-title-wrap {
display: flex;
align-items: baseline;
gap: 8px;
}
.section-emoji {
font-size: 30px;
}
.section-title {
font-size: 32px;
font-weight: 700;
color: #1a1a1a;
}
.section-subtitle {
font-size: 22px;
color: #969799;
margin-left: 4px;
}
.section-more {
display: flex;
align-items: center;
gap: 16px;
padding: 4px 0;
}
.join-icon { font-size: 48px; }
.section-more-text {
font-size: 24px;
color: #1989fa;
}
.join-text-wrap {
.join-title {
display: block;
font-size: 30px;
font-weight: 500;
color: #1a1a1a;
margin-bottom: 4px;
}
.section-more-arrow {
font-size: 28px;
color: #1989fa;
margin-left: 2px;
line-height: 1;
}
}
.join-desc {
font-size: 24px;
color: #969799;
}
.recommended-shops-list {
/* 卡片之间用 1px 顶边线分割 */
.shop-card {
padding: 20px 20px;
.shop-image-wrap {
width: 160px;
height: 160px;
}
}
}
.join-arrow {
font-size: 36px;
color: #1989fa;
font-weight: 300;
}
/* ========== 统一板块 ========== */
.main-tabs-section {
margin: 0 24px 20px;
background: #fff;
border-radius: 16px;
overflow: hidden;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
}
/* 文章列表内容 */
@@ -224,8 +204,11 @@
}
.meta-read {
flex: 1;
text-align: right;
margin-left: auto;
}
.meta-comment {
margin-left: 16px;
}
}
}
+51 -112
View File
@@ -3,20 +3,16 @@ import Taro from '@tarojs/taro'
import { View, Text, ScrollView } from '@tarojs/components'
import { Search, Swiper, SwiperItem, Grid, GridItem, Tabs, Tab, Image as VantImage } from '@antmjs/vantui'
import ArticleCard from '@/components/ArticleCard'
import ProductCard from '@/components/ProductCard'
import ShopCard from '@/components/ShopCard'
import { banners, navItems, articles } from '@/mock/articles'
import { products } from '@/mock/products'
import type { ArticleCategory } from '@/types/article'
import type { ProductCategory } from '@/types/product'
import type { Product } from '@/types/product'
import CustomTabBar from "@/custom-tab-bar";
import { getRecommendedShops } from '@/mock/shops'
import type { Shop } from '@/types/shop'
import './index.less'
/** 获取状态栏和导航栏高度 */
function getNavBarHeight() {
const systemInfo = Taro.getSystemInfoSync()
const statusBarHeight = systemInfo.statusBarHeight || 20
// 微信小程序导航栏高度一般为 44px,总高度 = 状态栏 + 导航栏
const navBarHeight = 44
return {
statusBarHeight,
@@ -25,45 +21,28 @@ function getNavBarHeight() {
}
}
/** 统一板块标签校园商城 + 文章分类 */
/** 文章板块标签校园商城 Tab 已移除) */
const MAIN_TABS = [
{ title: '校园商城', value: 'mall' },
{ title: '最新', value: 'latest' },
{ title: '推荐', value: 'recommend' },
{ title: '热点', value: 'hot' },
]
/** 商品分类标签 */
const PRODUCT_CATEGORIES: { title: string; value: ProductCategory | 'all' }[] = [
{ title: '全部', value: 'all' },
{ title: '数码', value: 'digital' },
{ title: '图书', value: 'book' },
{ title: '生活', value: 'life' },
{ title: '美食', value: 'food' },
{ title: '服饰', value: 'cloth' },
]
export default function Index() {
const [activeMainTab, setActiveMainTab] = useState(0)
const [activeCategory, setActiveCategory] = useState(0)
const navBarHeight = useMemo(() => getNavBarHeight(), [])
/** 当前选中的板块 */
const currentTab = MAIN_TABS[activeMainTab]?.value || 'mall'
const currentTab = MAIN_TABS[activeMainTab]?.value || 'latest'
/** 根据文章分类筛 */
const filteredArticles = useMemo(() => {
const value = MAIN_TABS[activeMainTab]?.value || 'latest'
if (value === 'mall') return articles.filter((a) => a.category === 'latest')
return articles.filter((a) => a.category === value)
}, [activeMainTab])
const filteredArticles = useMemo(
() => articles.filter((a) => a.category === currentTab),
[currentTab],
)
/** 根据当前分类筛选商品 */
const filteredProducts = useMemo(() => {
const catValue = PRODUCT_CATEGORIES[activeCategory]?.value || 'all'
if (catValue === 'all') return products
return products.filter((p) => p.category === catValue)
}, [activeCategory])
/** 推荐商家(首页显示前 3 个) */
const recommendedShops = useMemo(() => getRecommendedShops(3), [])
/** 搜索 */
const handleSearch = () => {
@@ -84,19 +63,14 @@ export default function Index() {
Taro.navigateTo({ url: `/pages/article/index?id=${id}` })
}
/** 商点击 */
const handleProductClick = (product: Product) => {
Taro.navigateTo({ url: `/pages/product-detail/index?id=${product.id}` })
/** 商点击 */
const handleShopClick = (shop: Shop) => {
Taro.navigateTo({ url: `/pages/shop-detail/index?id=${shop.id}` })
}
/** 查看全部商 */
const handleViewAll = () => {
Taro.showToast({ title: '商城即将上线', icon: 'none' })
}
/** 商家入驻 */
const handleMerchantJoin = () => {
Taro.showToast({ title: '商家入驻即将上线', icon: 'none' })
/** 查看全部商 */
const handleViewAllShops = () => {
Taro.navigateTo({ url: '/pages/food-delivery/index' })
}
return (
@@ -170,7 +144,27 @@ export default function Index() {
</Grid>
</View>
{/* ========== 统一板块:校园商城 | 最新 | 推荐 | 热点 ========== */}
{/* ========== 推荐商家 ========== */}
<View className='recommended-shops-section'>
<View className='section-header'>
<View className='section-title-wrap'>
<Text className='section-emoji'>🍱</Text>
<Text className='section-title'></Text>
<Text className='section-subtitle'></Text>
</View>
<View className='section-more' onClick={handleViewAllShops}>
<Text className='section-more-text'></Text>
<Text className='section-more-arrow'></Text>
</View>
</View>
<View className='recommended-shops-list'>
{recommendedShops.map((shop) => (
<ShopCard key={shop.id} shop={shop} onClick={handleShopClick} />
))}
</View>
</View>
{/* ========== 文章板块:最新 | 推荐 | 热点 ========== */}
<View className='main-tabs-section'>
<Tabs
active={activeMainTab}
@@ -188,81 +182,26 @@ export default function Index() {
))}
</Tabs>
{/* ===== 校园商城内容 ===== */}
{currentTab === 'mall' && (
<View className='mall-content'>
{/* 商品分类标签 */}
<ScrollView
className='category-scroll'
scrollX
enhanced
showScrollbar={false}
{/* 文章列表内容 */}
<View className='article-content'>
{filteredArticles.map((article) => (
<View
key={article.id}
className='article-item'
onClick={() => handleArticleClick(article.id)}
>
<View className='category-tabs'>
{PRODUCT_CATEGORIES.map((cat, idx) => (
<View
key={cat.value}
className={`category-tab ${activeCategory === idx ? 'active' : ''}`}
onClick={() => setActiveCategory(idx)}
>
<Text className='category-text'>{cat.title}</Text>
</View>
))}
</View>
</ScrollView>
{/* 商品双列网格 */}
<View className='product-grid'>
{filteredProducts.map((product) => (
<ProductCard
key={product.id}
product={product}
onClick={handleProductClick}
/>
))}
{filteredProducts.length === 0 && (
<View className='empty-grid'></View>
)}
<ArticleCard article={article} />
</View>
{/* 商家入驻入口 */}
<View className='merchant-join-card' onClick={handleMerchantJoin}>
<View className='join-content'>
<Text className='join-icon'>🏪</Text>
<View className='join-text-wrap'>
<Text className='join-title'></Text>
<Text className='join-desc'></Text>
</View>
</View>
<Text className='join-arrow'></Text>
</View>
</View>
)}
{/* ===== 文章列表内容 ===== */}
{currentTab !== 'mall' && (
<View className='article-content'>
{filteredArticles.map((article) => (
<View
key={article.id}
className='article-item'
onClick={() => handleArticleClick(article.id)}
>
<ArticleCard article={article} />
</View>
))}
{filteredArticles.length === 0 && (
<View className='empty-tip'></View>
)}
</View>
)}
))}
{filteredArticles.length === 0 && (
<View className='empty-tip'></View>
)}
</View>
</View>
{/* 底部安全距离 */}
<View className='bottom-safe' />
</ScrollView>
{/*<CustomTabBar activeKey='index' />*/}
</View>
)
}
-237
View File
@@ -1,237 +0,0 @@
/* ========================================
商品详情页
======================================== */
.product-detail-page {
min-height: 100vh;
background: #f7f8fa;
display: flex;
flex-direction: column;
}
/* ========== 导航栏 ========== */
.detail-nav {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
.nav-content {
height: 44px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 24px;
.nav-back {
width: 60px;
height: 60px;
display: flex;
align-items: center;
justify-content: center;
margin-left: -10px;
}
.nav-title {
font-size: 32px;
font-weight: 500;
color: #1a1a1a;
}
.nav-actions {
display: flex;
gap: 8px;
width: 50px;
justify-content: flex-end;
}
}
}
/* ========== 滚动区域 ========== */
.detail-scroll {
flex: 1;
}
/* ========== 图片轮播 ========== */
.image-swiper {
width: 100%;
height: 750px;
background: #fff;
.swiper-image {
width: 100%;
height: 100%;
}
}
/* ========== 价格信息卡片 ========== */
.price-card {
background: #fff;
padding: 24px;
margin-bottom: 16px;
.price-row {
display: flex;
align-items: baseline;
gap: 12px;
margin-bottom: 16px;
.current-price {
font-size: 48px;
font-weight: 700;
color: #ee0a24;
line-height: 1;
}
.original-price {
font-size: 28px;
color: #c8c9cc;
text-decoration: line-through;
}
.discount-badge {
font-size: 24px;
color: #ee0a24;
background: #fff0f0;
padding: 2px 10px;
border-radius: 6px;
}
}
.product-name {
display: block;
font-size: 34px;
font-weight: 600;
color: #1a1a1a;
line-height: 1.4;
margin-bottom: 16px;
}
.product-tags {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 16px;
}
.product-sales {
font-size: 26px;
color: #969799;
}
}
/* ========== 规格选择行 ========== */
.sku-select-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 24px;
background: #fff;
margin-bottom: 16px;
.sku-label {
font-size: 28px;
color: #646566;
}
.sku-value {
display: flex;
align-items: center;
font-size: 28px;
color: #1a1a1a;
}
}
/* ========== 商家信息 ========== */
.merchant-card {
display: flex;
align-items: center;
justify-content: space-between;
padding: 24px;
background: #fff;
margin-bottom: 16px;
.merchant-info {
display: flex;
align-items: center;
gap: 16px;
.merchant-avatar {
width: 72px;
height: 72px;
border-radius: 50%;
}
.merchant-name {
font-size: 28px;
font-weight: 500;
color: #1a1a1a;
}
}
.merchant-arrow {
font-size: 28px;
color: #c8c9cc;
}
}
/* ========== 商品描述 ========== */
.desc-card {
background: #fff;
padding: 24px;
margin-bottom: 16px;
.desc-title {
display: block;
font-size: 30px;
font-weight: 600;
color: #1a1a1a;
margin-bottom: 16px;
}
.desc-content {
display: block;
font-size: 28px;
color: #646566;
line-height: 1.8;
}
}
/* ========== 推荐商品 ========== */
.recommend-section {
padding: 0 24px 16px;
.section-title {
display: block;
font-size: 32px;
font-weight: 600;
color: #1a1a1a;
margin-bottom: 16px;
padding-left: 8px;
border-left: 6px solid #1989fa;
}
}
.recommend-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
.empty-recommend {
grid-column: 1 / -1;
text-align: center;
padding: 40px 0;
font-size: 28px;
color: #c8c9cc;
}
}
/* ========== 底部安全区 ========== */
.scroll-bottom-safe {
height: 120px;
}
-411
View File
@@ -1,411 +0,0 @@
import { useState, useMemo, useCallback } from 'react'
import Taro from '@tarojs/taro'
import { View, Text, ScrollView } from '@tarojs/components'
import {
Swiper,
SwiperItem,
Tag,
Image as VantImage,
Icon,
GoodsAction,
GoodsActionIcon,
GoodsActionButton,
Stepper,
Sku,
Popup,
Button,
} from '@antmjs/vantui'
import type { ISkuItem, IGoodItem } from '@antmjs/vantui/types/sku'
import { products } from '@/mock/products'
import ProductCard from '@/components/ProductCard'
import type { Product } from '@/types/product'
import './index.less'
/* ==============================
============================== */
function getStatusBarH(): number {
try { return Taro.getSystemInfoSync().statusBarHeight || 20 }
catch { return 20 }
}
function getSafeBottom(): number {
try {
const info = Taro.getSystemInfoSync()
return (info.screenHeight - info.safeArea!.bottom) || 0
} catch { return 0 }
}
/** 根据商品生成简单 SKU 数据 */
function buildSkuData(product: Product) {
const goodsList: IGoodItem[] = [
{
id: 1,
skuIds: [1, 11],
price: Math.round(product.price * 100),
stock: 999,
},
{
id: 2,
skuIds: [1, 12],
price: Math.round(product.price * 100),
stock: 0,
},
{
id: 3,
skuIds: [2, 11],
price: Math.round(product.price * 100) + 1000,
stock: 500,
},
{
id: 4,
skuIds: [2, 12],
price: Math.round(product.price * 100) + 1000,
stock: 300,
},
]
const sku: ISkuItem[] = [
{
id: 1,
name: '颜色',
items: [
{ id: 1, name: '黑色' },
{ id: 2, name: '白色' },
],
},
{
id: 2,
name: '版本',
items: [
{ id: 11, name: '标准版' },
{ id: 12, name: '高配版' },
],
},
]
return { goodsList, sku }
}
/* ==============================
============================== */
export default function ProductDetail() {
const id = String(Taro.getCurrentInstance().router?.params?.id || '')
/* ---- 数据 ---- */
const product = useMemo(() => products.find((p) => p.id === id), [id])
const statusBarH = useMemo(getStatusBarH, [])
const safeBottom = useMemo(getSafeBottom, [])
const [isFavorited, setIsFavorited] = useState(false)
/* ---- SKU 弹窗 ---- */
const [skuVisible, setSkuVisible] = useState(false)
const [skuAction, setSkuAction] = useState<'cart' | 'buy'>('cart')
const [quantity, setQuantity] = useState(1)
const skuData = useMemo(() => {
if (!product) return { goodsList: [], sku: [] }
return buildSkuData(product)
}, [product])
/* ---- 推荐商品 ---- */
const recommendedProducts = useMemo(() => {
if (!product) return []
return products
.filter((p) => p.category === product.category && p.id !== product.id)
.slice(0, 4)
}, [product])
/* ---- 事件 ---- */
const handleBack = useCallback(() => { Taro.navigateBack() }, [])
const handleNavigateToDetail = useCallback((targetId: string) => {
Taro.navigateTo({ url: `/pages/product-detail/index?id=${targetId}` })
}, [])
/** 打开 SKU 弹窗 */
const openSku = (action: 'cart' | 'buy') => {
setSkuAction(action)
setSkuVisible(true)
}
const handleSkuClose = () => setSkuVisible(false)
/** SKU 确认 */
const handleSkuConfirm = () => {
setSkuVisible(false)
if (skuAction === 'cart') {
Taro.showToast({ title: '已加入购物车', icon: 'success' })
} else {
Taro.showToast({ title: '下单成功', icon: 'success' })
}
setQuantity(1)
}
/* ---- 空状态 ---- */
if (!product) {
return (
<View className='product-detail-page'>
<View className='detail-nav' style={{ paddingTop: `${statusBarH}px` }}>
<View className='nav-content'>
<View className='nav-back' onClick={handleBack}>
<Icon name='arrow-left' size={22} color='#323233' />
</View>
<Text className='nav-title'></Text>
<View className='nav-actions' />
</View>
</View>
<View style={{ height: `${statusBarH + 44}px` }} />
<View style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', flex: 1 }}>
<Text style={{ color: '#969799', fontSize: '30px' }}></Text>
</View>
</View>
)
}
const discount = product.originalPrice
? Math.round((product.price / product.originalPrice) * 10)
: null
return (
<View className='product-detail-page'>
{/* ===== 导航栏 ===== */}
<View className='detail-nav' style={{ paddingTop: `${statusBarH}px` }}>
<View className='nav-content'>
<View className='nav-back' onClick={handleBack}>
<Icon name='arrow-left' size={22} color='#323233' />
</View>
<Text className='nav-title'></Text>
<View className='nav-actions' />
</View>
</View>
<View style={{ height: `${statusBarH + 44}px` }} />
{/* ===== 滚动内容 ===== */}
<ScrollView className='detail-scroll' scrollY enhanced showScrollbar={false}>
{/* 图片轮播 — 使用商品图片 + 模拟多张 */}
<Swiper
className='image-swiper'
autoPlay={0}
loop
paginationVisible
paginationColor='#1989fa'
height={750}
>
{[product.image, product.image, product.image].map((img, idx) => (
<SwiperItem key={idx}>
<VantImage
className='swiper-image'
src={img}
fit='cover'
width='100%'
height='100%'
/>
</SwiperItem>
))}
</Swiper>
{/* 价格信息 */}
<View className='price-card'>
<View className='price-row'>
<Text className='current-price'>¥{product.price.toFixed(2)}</Text>
{product.originalPrice && (
<Text className='original-price'>¥{product.originalPrice.toFixed(2)}</Text>
)}
{discount && discount < 10 && (
<Text className='discount-badge'>{discount}</Text>
)}
</View>
<Text className='product-name'>{product.title}</Text>
{product.tags && product.tags.length > 0 && (
<View className='product-tags'>
{product.tags.map((t) => (
<Tag key={t} type='danger' plain>
{t}
</Tag>
))}
</View>
)}
<Text className='product-sales'> {product.sales}</Text>
</View>
{/* 规格选择 */}
<View className='sku-select-row' onClick={() => openSku('cart')}>
<Text className='sku-label'></Text>
<View className='sku-value'>
<Text>颜色: 黑色 / 版本: 标准版 ×{quantity}</Text>
<Icon name='arrow' size={16} color='#c8c9cc' />
</View>
</View>
{/* 商家信息 */}
<View className='merchant-card'>
<View className='merchant-info'>
<VantImage
className='merchant-avatar'
src={product.merchant.avatar}
fit='cover'
width={72}
height={72}
round
/>
<Text className='merchant-name'>{product.merchant.name}</Text>
</View>
<Icon name='arrow' size={16} color='#c8c9cc' />
</View>
{/* 商品描述 */}
<View className='desc-card'>
<Text className='desc-title'></Text>
<Text className='desc-content'>{product.desc}</Text>
</View>
{/* 推荐商品 */}
<View className='recommend-section'>
<Text className='section-title'></Text>
<View className='recommend-grid'>
{recommendedProducts.length > 0 ? (
recommendedProducts.map((p) => (
<ProductCard
key={p.id}
product={p}
onClick={(prod) => handleNavigateToDetail(prod.id)}
/>
))
) : (
<Text className='empty-recommend'></Text>
)}
</View>
</View>
<View className='scroll-bottom-safe' />
</ScrollView>
{/* ===== 底部操作栏 ===== */}
<GoodsAction safeAreaInsetBottom={false}>
<GoodsActionIcon
icon='chat-o'
text='客服'
onClick={() => Taro.showToast({ title: '联系客服中...', icon: 'none' })}
/>
<GoodsActionIcon
icon={isFavorited ? 'star' : 'star-o'}
text='收藏'
color={isFavorited ? '#ff976a' : undefined}
onClick={() => {
setIsFavorited(!isFavorited)
Taro.showToast({
title: isFavorited ? '已取消收藏' : '已收藏',
icon: 'none',
})
}}
/>
<GoodsActionIcon
icon='cart-o'
text='购物车'
info='0'
onClick={() => Taro.showToast({ title: '购物车即将上线', icon: 'none' })}
/>
<GoodsActionButton
text='加入购物车'
type='warning'
onClick={() => openSku('cart')}
/>
<GoodsActionButton
text='立即购买'
type='danger'
onClick={() => openSku('buy')}
/>
</GoodsAction>
{/* ===== SKU 选择弹窗 ===== */}
<Popup
show={skuVisible}
position='bottom'
round
rootPortal
onClose={handleSkuClose}
overlayStyle={{ backgroundColor: 'rgba(0,0,0,0.5)' }}
>
<View style={{ background: '#fff', paddingBottom: `${safeBottom}px` }}>
{/* 弹窗头部 — 商品概要 */}
<View style={{ display: 'flex', padding: '24px', borderBottom: '1px solid #f2f3f5' }}>
<VantImage
src={product.image}
fit='cover'
width={160}
height={160}
radius={8}
/>
<View style={{ marginLeft: '20px', flex: 1 }}>
<Text style={{ fontSize: '40px', fontWeight: 700, color: '#ee0a24', display: 'block' }}>
¥{product.price.toFixed(2)}
</Text>
<Text
style={{
fontSize: '24px',
color: '#969799',
display: 'block',
marginTop: '8px',
}}
>
999
</Text>
</View>
</View>
{/* SKU 规格选择 */}
<View style={{ padding: '0 24px', maxHeight: '500px', overflowY: 'auto' }}>
<Sku
goodsId={1}
goodsList={skuData.goodsList}
sku={skuData.sku}
onChange={(goods) => {
if (goods) {
// SKU 变化时更新价格
}
}}
/>
</View>
{/* 数量选择 */}
<View
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '24px',
borderTop: '1px solid #f2f3f5',
borderBottom: '1px solid #f2f3f5',
}}
>
<Text style={{ fontSize: '28px', color: '#646566' }}></Text>
<Stepper
value={quantity}
min={1}
max={99}
onChange={(e) => setQuantity(Number(e.detail))}
/>
</View>
{/* 确认按钮 */}
<View style={{ padding: '24px' }}>
<Button
type={skuAction === 'buy' ? 'danger' : 'warning'}
round
block
onClick={handleSkuConfirm}
>
{skuAction === 'buy' ? '立即购买' : '加入购物车'}
</Button>
</View>
</View>
</Popup>
</View>
)
}
+4
View File
@@ -0,0 +1,4 @@
export default definePageConfig({
navigationStyle: 'custom',
navigationBarTitleText: '搜索',
})
+194
View File
@@ -0,0 +1,194 @@
/* ========================================
搜索页
======================================== */
.search-page {
min-height: 100vh;
background: #f7f8fa;
}
/* ========== 自定义导航栏 ========== */
.custom-navbar {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 999;
background: #fff;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.04);
}
.navbar-content {
display: flex;
align-items: center;
padding: 0 16px;
gap: 12px;
}
.navbar-back {
width: 60px;
height: 100%;
display: flex;
align-items: center;
flex-shrink: 0;
.back-icon {
font-size: 56px;
color: #1a1a1a;
line-height: 1;
}
}
.navbar-search {
flex: 1;
min-width: 0;
}
.navbar-search-btn {
flex-shrink: 0;
padding: 8px 8px;
.search-btn-text {
font-size: 28px;
color: #1989fa;
font-weight: 500;
}
}
/* ========== 滚动区域 ========== */
.search-scroll {
height: calc(100vh - 100px);
}
/* ========== section 通用 ========== */
.section {
margin: 20px 24px;
background: #fff;
border-radius: 16px;
padding: 20px 24px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
}
.section-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20px;
.section-title {
font-size: 30px;
font-weight: 600;
color: #1a1a1a;
}
.clear-btn {
padding: 4px;
.clear-icon {
font-size: 28px;
color: #969799;
}
}
}
.tag-list {
display: flex;
flex-wrap: wrap;
gap: 12px;
}
.tag-item {
display: flex;
align-items: center;
gap: 6px;
padding: 10px 20px;
background: #f7f8fa;
border-radius: 32px;
transition: opacity 0.2s;
&:active {
opacity: 0.6;
}
&.hot {
background: #fff7e6;
}
.rank {
font-size: 24px;
font-weight: 700;
color: #ff6034;
}
.tag-text {
font-size: 26px;
color: #646566;
}
}
/* ========== 搜索结果 ========== */
.results-section {
padding: 0 24px;
}
.results-header {
display: flex;
align-items: baseline;
justify-content: space-between;
padding: 20px 0 16px;
.results-title {
font-size: 28px;
color: #646566;
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.results-keyword {
color: #1989fa;
font-weight: 600;
}
.results-count {
font-size: 24px;
color: #969799;
flex-shrink: 0;
margin-left: 12px;
}
}
.results-list {
display: flex;
flex-direction: column;
gap: 16px;
}
.result-post-wrap {
/* ForumPostCard 自带 padding 和 shadow */
}
.empty-tip {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 120px 0;
.empty-icon {
font-size: 80px;
margin-bottom: 24px;
opacity: 0.4;
}
.empty-text {
font-size: 28px;
color: #969799;
}
}
.bottom-safe {
height: 40px;
}
+227
View File
@@ -0,0 +1,227 @@
import { useState, useMemo, useEffect } from 'react'
import Taro from '@tarojs/taro'
import { View, Text, ScrollView } from '@tarojs/components'
import { Search, Tag } from '@antmjs/vantui'
import { shops } from '@/mock/shops'
import { forumPosts } from '@/mock/forum'
import ShopCard from '@/components/ShopCard'
import ForumPostCard from '@/components/ForumPostCard'
import type { Article } from '@/types/article'
import type { Shop } from '@/types/shop'
import './index.less'
/** 状态栏 + 导航栏高度 */
function getNavBarHeight() {
try {
const sys = Taro.getSystemInfoSync()
return { statusBarHeight: sys.statusBarHeight || 20, navBarHeight: 44 }
} catch {
return { statusBarHeight: 20, navBarHeight: 44 }
}
}
/** 搜索结果项的联合类型 */
type SearchResult =
| { kind: 'shop'; data: Shop }
| { kind: 'post'; data: Article }
export default function SearchPage() {
const [keyword, setKeyword] = useState('')
const [submittedKeyword, setSubmittedKeyword] = useState('')
const [history, setHistory] = useState<string[]>(['麻辣烫', '二手 iPad', '考研资料'])
const [navBarHeight, setNavBarHeight] = useState({ statusBarHeight: 20, navBarHeight: 44 })
useEffect(() => {
setNavBarHeight(getNavBarHeight())
}, [])
/** 热门搜索 */
const hotKeywords = ['麻辣烫', '奶茶', '二手 iPad', '考研资料', '黄焖鸡', '失物招领', '六级', '外卖红包']
/** 执行搜索(综合匹配商家和帖子) */
const results = useMemo<SearchResult[]>(() => {
if (!submittedKeyword.trim()) return []
const kw = submittedKeyword.toLowerCase().trim()
const matchedShops: Shop[] = shops.filter(
(s) =>
s.name.toLowerCase().includes(kw) ||
s.description.toLowerCase().includes(kw) ||
s.tags.some((t) => t.toLowerCase().includes(kw)),
)
const matchedPosts: Article[] = forumPosts.filter(
(p) =>
p.title.toLowerCase().includes(kw) ||
p.excerpt.toLowerCase().includes(kw) ||
p.source.toLowerCase().includes(kw),
)
return [
...matchedShops.map((s) => ({ kind: 'shop' as const, data: s })),
...matchedPosts.map((p) => ({ kind: 'post' as const, data: p })),
]
}, [submittedKeyword])
/** 返回 */
const handleBack = () => {
Taro.navigateBack()
}
/** 点击搜索 */
const handleSearch = () => {
const kw = keyword.trim()
if (!kw) {
Taro.showToast({ title: '请输入搜索关键词', icon: 'none' })
return
}
setSubmittedKeyword(kw)
// 保存到历史(去重,最多 8 条)
setHistory((prev) => {
const next = [kw, ...prev.filter((h) => h !== kw)]
return next.slice(0, 8)
})
}
/** 点击历史/热门 */
const handleQuickKeyword = (kw: string) => {
setKeyword(kw)
setSubmittedKeyword(kw)
setHistory((prev) => {
const next = [kw, ...prev.filter((h) => h !== kw)]
return next.slice(0, 8)
})
}
/** 清空历史 */
const handleClearHistory = () => {
setHistory([])
}
/** 商家点击 */
const handleShopClick = (shop: Shop) => {
Taro.navigateTo({ url: `/pages/shop-detail/index?id=${shop.id}` })
}
/** 帖子点击 */
const handlePostClick = (post: Article) => {
Taro.navigateTo({ url: `/pages/article/index?id=${post.id}` })
}
return (
<View className='search-page'>
{/* ========== 自定义导航栏(带返回 + 搜索框 + 搜索按钮) ========== */}
<View
className='custom-navbar'
style={{ paddingTop: `${navBarHeight.statusBarHeight}px` }}
>
<View
className='navbar-content'
style={{ height: `${navBarHeight.navBarHeight}px` }}
>
<View className='navbar-back' onClick={handleBack}>
<Text className='back-icon'></Text>
</View>
<View className='navbar-search'>
<Search
value={keyword}
placeholder='搜索商家、菜品、帖子'
shape='round'
background='#f2f3f5'
onChange={(e) => setKeyword(e.detail.value)}
onSearch={handleSearch}
/>
</View>
<View className='navbar-search-btn' onClick={handleSearch}>
<Text className='search-btn-text'></Text>
</View>
</View>
</View>
{/* 占位 */}
<View style={{ height: `${navBarHeight.statusBarHeight + navBarHeight.navBarHeight}px` }} />
<ScrollView className='search-scroll' scrollY enhanced showScrollbar={false}>
{/* ========== 搜索结果(已提交关键词时显示) ========== */}
{submittedKeyword ? (
<View className='results-section'>
<View className='results-header'>
<Text className='results-title'>
"<Text className='results-keyword'>{submittedKeyword}</Text>"
</Text>
<Text className='results-count'> {results.length} </Text>
</View>
{results.length > 0 ? (
<View className='results-list'>
{results.map((r) =>
r.kind === 'shop' ? (
<ShopCard
key={`shop-${r.data.id}`}
shop={r.data}
onClick={handleShopClick}
/>
) : (
<View key={`post-${r.data.id}`} className='result-post-wrap'>
<ForumPostCard post={r.data} onClick={handlePostClick} />
</View>
),
)}
</View>
) : (
<View className='empty-tip'>
<Text className='empty-icon'>🔍</Text>
<Text className='empty-text'></Text>
</View>
)}
</View>
) : (
<>
{/* ========== 搜索历史 ========== */}
{history.length > 0 && (
<View className='section'>
<View className='section-header'>
<Text className='section-title'></Text>
<View className='clear-btn' onClick={handleClearHistory}>
<Text className='clear-icon'>🗑</Text>
</View>
</View>
<View className='tag-list'>
{history.map((h) => (
<View
key={h}
className='tag-item'
onClick={() => handleQuickKeyword(h)}
>
<Text className='tag-text'>{h}</Text>
</View>
))}
</View>
</View>
)}
{/* ========== 热门搜索 ========== */}
<View className='section'>
<View className='section-header'>
<Text className='section-title'></Text>
</View>
<View className='tag-list'>
{hotKeywords.map((kw, idx) => (
<View
key={kw}
className={`tag-item ${idx < 3 ? 'hot' : ''}`}
onClick={() => handleQuickKeyword(kw)}
>
{idx < 3 && <Text className='rank'>{idx + 1}</Text>}
<Text className='tag-text'>{kw}</Text>
</View>
))}
</View>
</View>
</>
)}
<View className='bottom-safe' />
</ScrollView>
</View>
)
}
+4
View File
@@ -0,0 +1,4 @@
export default definePageConfig({
navigationStyle: 'custom',
navigationBarTitleText: '',
})
+870
View File
@@ -0,0 +1,870 @@
/* ========================================
店铺首页
======================================== */
.shop-detail-page {
min-height: 100vh;
background: #f7f8fa;
padding-bottom: 120px; /* 留出 SubmitBar 空间 */
}
/* ========== 自定义导航栏 ========== */
.custom-navbar {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 1000;
background: rgba(255, 255, 255, 0.92);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border-bottom: 1px solid #f2f3f5;
}
.navbar-content {
display: flex;
align-items: center;
padding: 0 16px;
position: relative;
}
.navbar-back {
width: 60px;
height: 100%;
display: flex;
align-items: center;
.back-icon {
font-size: 56px;
color: #1a1a1a;
line-height: 1;
}
}
.navbar-title-wrap {
flex: 1;
display: flex;
justify-content: center;
.navbar-title {
font-size: 32px;
font-weight: 600;
color: #1a1a1a;
max-width: 400px;
}
}
.navbar-action {
width: 60px;
display: flex;
justify-content: flex-end;
.action-icon {
font-size: 36px;
color: #1a1a1a;
}
}
/* ========== 店铺头 ========== */
.shop-header {
position: relative;
background: #fff;
margin-bottom: 16px;
}
.header-bg {
position: relative;
width: 100%;
height: 200px;
overflow: hidden;
.header-bg-image {
width: 100%;
height: 100%;
transform: scale(1.2);
filter: blur(2px);
}
.header-bg-mask {
position: absolute;
inset: 0;
background: linear-gradient(180deg, rgba(0, 0, 0, 0.2) 0%, rgba(255, 255, 255, 1) 100%);
}
}
.header-content {
display: flex;
align-items: flex-end;
padding: 0 24px 20px;
margin-top: -50px;
position: relative;
z-index: 1;
}
.header-logo-wrap {
width: 130px;
height: 130px;
border-radius: 16px;
overflow: hidden;
border: 4px solid #fff;
flex-shrink: 0;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
.header-logo {
width: 100%;
height: 100%;
}
}
.header-info {
flex: 1;
margin-left: 20px;
padding-bottom: 8px;
min-width: 0;
}
.header-name-row {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 8px;
.header-name {
font-size: 36px;
font-weight: 700;
color: #1a1a1a;
}
.header-tag {
margin: 0;
}
}
.header-rating-row {
display: flex;
align-items: center;
gap: 6px;
margin-bottom: 6px;
font-size: 24px;
color: #646566;
.rating-value {
color: #ff6034;
font-weight: 600;
margin-left: 2px;
}
.header-sep {
color: #c8c9cc;
}
.sales {
color: #646566;
}
}
.header-fee-row {
display: flex;
align-items: center;
gap: 6px;
font-size: 22px;
color: #969799;
.fee-sep {
color: #c8c9cc;
}
}
.header-announcement {
display: flex;
align-items: flex-start;
gap: 12px;
margin: 0 24px 20px;
padding: 16px 20px;
background: #fff7e6;
border-radius: 8px;
position: relative;
z-index: 1;
.announcement-label {
flex-shrink: 0;
font-size: 22px;
font-weight: 600;
color: #ff976a;
background: #fff;
padding: 2px 8px;
border-radius: 4px;
}
.announcement-text {
flex: 1;
font-size: 24px;
color: #646566;
line-height: 1.4;
}
}
/* ========== 点菜 Tab ========== */
.menu-tab {
background: #fff;
}
.menu-body {
display: flex;
height: calc(100vh - 200px);
min-height: 600px;
}
.menu-sidebar {
width: 200px;
background: #f7f8fa;
flex-shrink: 0;
}
.menu-list-scroll {
flex: 1;
height: 100%;
background: #fff;
}
.menu-category {
padding: 20px 20px 4px;
& + .menu-category {
border-top: 8px solid #f7f8fa;
}
}
.category-header {
display: flex;
align-items: baseline;
justify-content: space-between;
padding: 8px 0 16px;
.category-name {
font-size: 30px;
font-weight: 600;
color: #1a1a1a;
}
.category-count {
font-size: 22px;
color: #969799;
}
}
.menu-item {
display: flex;
padding: 16px 0;
& + .menu-item {
border-top: 1px solid #f2f3f5;
}
}
.menu-item-image-wrap {
flex-shrink: 0;
width: 180px;
height: 180px;
border-radius: 8px;
overflow: hidden;
margin-right: 16px;
.menu-item-image {
width: 100%;
height: 100%;
}
}
.menu-item-info {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.menu-item-name-row {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
.menu-item-name {
font-size: 28px;
font-weight: 500;
color: #1a1a1a;
}
.menu-item-tag {
margin: 0;
}
}
.menu-item-desc {
font-size: 22px;
color: #969799;
margin-top: 6px;
line-height: 1.3;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
overflow: hidden;
}
.menu-item-sales {
font-size: 22px;
color: #969799;
margin-top: 4px;
}
.menu-item-bottom {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 8px;
}
.price-row {
display: flex;
align-items: baseline;
gap: 8px;
.menu-item-price {
font-size: 32px;
font-weight: 700;
color: #ee0a24;
line-height: 1;
}
.menu-item-original {
font-size: 22px;
color: #c8c9cc;
text-decoration: line-through;
}
}
.stepper-row {
display: flex;
align-items: center;
gap: 4px;
}
.stepper-btn {
width: 44px;
height: 44px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
background: #1989fa;
transition: opacity 0.2s;
&:active {
opacity: 0.7;
}
&.minus {
background: #fff;
border: 2px solid #1989fa;
}
&.plus {
background: #1989fa;
}
.stepper-btn-text {
font-size: 32px;
line-height: 1;
color: #1989fa;
font-weight: 600;
&.plus {
color: #fff;
}
}
}
.stepper-qty {
min-width: 40px;
text-align: center;
font-size: 28px;
color: #1a1a1a;
font-weight: 500;
}
/* ========== 评价 Tab ========== */
.reviews-tab {
background: #f7f8fa;
}
.reviews-scroll {
height: calc(100vh - 200px);
min-height: 600px;
padding: 0 24px;
}
.reviews-summary {
display: flex;
align-items: center;
background: #fff;
border-radius: 12px;
padding: 24px 32px;
margin: 16px 0;
.summary-rating {
display: flex;
flex-direction: column;
align-items: center;
padding-right: 32px;
border-right: 1px solid #f2f3f5;
.summary-rating-value {
font-size: 48px;
font-weight: 700;
color: #ff6034;
line-height: 1.2;
}
.summary-rating-label {
font-size: 22px;
color: #969799;
margin-top: 4px;
}
}
.summary-stats {
flex: 1;
display: flex;
justify-content: space-around;
}
.stat-item {
display: flex;
flex-direction: column;
align-items: center;
.stat-num {
font-size: 32px;
font-weight: 600;
color: #1a1a1a;
line-height: 1.2;
}
.stat-label {
font-size: 22px;
color: #969799;
margin-top: 4px;
}
}
}
.reviews-list {
background: #fff;
border-radius: 12px;
overflow: hidden;
}
.review-card {
padding: 24px;
& + .review-card {
border-top: 1px solid #f2f3f5;
}
}
.review-header {
display: flex;
align-items: center;
margin-bottom: 12px;
}
.review-avatar {
width: 64px;
height: 64px;
flex-shrink: 0;
margin-right: 12px;
}
.review-meta {
flex: 1;
.review-name {
font-size: 26px;
color: #1a1a1a;
font-weight: 500;
}
.review-rate-row {
display: flex;
align-items: center;
gap: 8px;
margin-top: 4px;
}
.review-time {
font-size: 22px;
color: #969799;
}
}
.review-content {
font-size: 28px;
color: #1a1a1a;
line-height: 1.5;
}
.review-images {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 12px;
}
.review-image-wrap {
width: 160px;
height: 160px;
border-radius: 6px;
overflow: hidden;
.review-image {
width: 100%;
height: 100%;
}
}
.review-dishes {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 12px;
.review-dish-tag {
margin: 0;
}
}
/* ========== 商家 Tab ========== */
.merchant-tab {
background: #f7f8fa;
}
.merchant-scroll {
height: calc(100vh - 200px);
min-height: 600px;
padding: 16px 24px;
}
.merchant-section {
background: #fff;
border-radius: 12px;
padding: 24px;
margin-bottom: 16px;
.merchant-section-title {
display: block;
font-size: 30px;
font-weight: 600;
color: #1a1a1a;
margin-bottom: 16px;
}
}
.merchant-photos {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 8px;
}
.merchant-photo-wrap {
width: 100%;
height: 180px;
border-radius: 8px;
overflow: hidden;
.merchant-photo {
width: 100%;
height: 100%;
}
}
.merchant-tags {
display: flex;
flex-wrap: wrap;
gap: 8px;
.merchant-tag {
margin: 0;
}
}
.merchant-info-list {
.merchant-info-row {
display: flex;
padding: 12px 0;
border-bottom: 1px solid #f2f3f5;
&:last-child {
border-bottom: none;
}
.info-label {
flex-shrink: 0;
width: 140px;
font-size: 26px;
color: #969799;
}
.info-value {
flex: 1;
font-size: 26px;
color: #1a1a1a;
}
}
}
.merchant-announcement {
padding: 16px;
background: #fff7e6;
border-radius: 8px;
.announcement-text {
font-size: 26px;
color: #646566;
line-height: 1.5;
}
}
/* ========== 底部购物车条 ========== */
.submit-bar-wrap {
position: fixed;
left: 0;
right: 0;
bottom: 0;
height: 100px;
background: #2c2c2c;
display: flex;
align-items: center;
padding: 0 16px;
z-index: 100;
}
.cart-icon-btn {
position: relative;
width: 100px;
height: 100px;
background: #1989fa;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-top: -30px;
box-shadow: 0 4px 12px rgba(25, 137, 250, 0.4);
.cart-icon {
font-size: 40px;
}
}
.cart-badge {
position: absolute;
top: 4px;
right: 4px;
min-width: 32px;
height: 32px;
border-radius: 16px;
background: #ee0a24;
color: #fff;
display: flex;
align-items: center;
justify-content: center;
padding: 0 6px;
border: 2px solid #2c2c2c;
.cart-badge-text {
font-size: 20px;
font-weight: 600;
line-height: 1;
}
}
.cart-info {
flex: 1;
margin-left: 16px;
color: #fff;
.cart-price {
font-size: 32px;
font-weight: 700;
line-height: 1.2;
}
.cart-fee {
font-size: 22px;
color: #c8c9cc;
margin-top: 2px;
}
}
.submit-btn {
height: 80px;
padding: 0 32px;
border-radius: 40px;
display: flex;
align-items: center;
justify-content: center;
&.active {
background: #1989fa;
}
&.disabled {
background: #646566;
}
.submit-btn-text {
font-size: 28px;
font-weight: 600;
color: #fff;
}
}
/* ========== 购物车 Popup ========== */
.cart-sheet {
display: flex;
flex-direction: column;
max-height: 80vh;
}
.cart-sheet-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 24px 32px 16px;
border-bottom: 1px solid #f2f3f5;
.cart-sheet-title {
font-size: 32px;
font-weight: 600;
color: #1a1a1a;
}
.cart-clear {
padding: 8px 0;
.cart-clear-text {
font-size: 24px;
color: #969799;
}
}
}
.cart-sheet-list {
flex: 1;
min-height: 200px;
max-height: 600px;
padding: 0 32px;
}
.cart-row {
display: flex;
align-items: center;
padding: 20px 0;
border-bottom: 1px solid #f2f3f5;
}
.cart-row-image-wrap {
width: 100px;
height: 100px;
border-radius: 6px;
overflow: hidden;
flex-shrink: 0;
}
.cart-row-info {
flex: 1;
margin: 0 20px;
min-width: 0;
.cart-row-name {
display: block;
font-size: 26px;
color: #1a1a1a;
font-weight: 500;
}
.cart-row-price {
display: block;
font-size: 28px;
color: #ee0a24;
font-weight: 600;
margin-top: 8px;
}
}
.cart-sheet-empty {
text-align: center;
padding: 60px 0;
font-size: 26px;
color: #c8c9cc;
}
.cart-sheet-footer {
display: flex;
align-items: center;
padding: 20px 32px;
border-top: 1px solid #f2f3f5;
background: #fff;
}
.cart-sheet-total {
flex: 1;
.cart-sheet-total-label {
font-size: 24px;
color: #969799;
}
.cart-sheet-total-price {
font-size: 36px;
font-weight: 700;
color: #ee0a24;
margin-left: 8px;
}
.cart-sheet-total-fee {
display: block;
font-size: 22px;
color: #969799;
margin-top: 2px;
}
}
.cart-sheet-submit {
padding: 16px 40px;
border-radius: 32px;
background: #1989fa;
&.disabled {
background: #c8c9cc;
}
.cart-sheet-submit-text {
font-size: 28px;
font-weight: 600;
color: #fff;
}
}
/* ========== 通用 ========== */
.empty-tip {
text-align: center;
padding: 60px 0;
font-size: 26px;
color: #c8c9cc;
}
.bottom-safe {
height: 40px;
}
+643
View File
@@ -0,0 +1,643 @@
import { useState, useMemo, useEffect } from 'react'
import Taro from '@tarojs/taro'
import { View, Text, ScrollView } from '@tarojs/components'
import {
Tabs,
Tab,
Sidebar,
SidebarItem,
Image as VantImage,
Tag,
Rate,
Stepper,
Popup,
} from '@antmjs/vantui'
import { getShopById } from '@/mock/shops'
import { getMenuCategories, getMenuItems } from '@/mock/menus'
import { getShopReviews } from '@/mock/reviews'
import type { Shop, MenuItem, MenuCategory, Review } from '@/types/shop'
import './index.less'
/** 状态栏 + 导航栏高度 */
function getNavBarHeight() {
const systemInfo = Taro.getSystemInfoSync()
const statusBarHeight = systemInfo.statusBarHeight || 20
const navBarHeight = 44
return { statusBarHeight, navBarHeight, totalHeight: statusBarHeight + navBarHeight }
}
export default function ShopDetail() {
// === 路由参数 ===
const [shop, setShop] = useState<Shop | null>(null)
const [categories, setCategories] = useState<MenuCategory[]>([])
const [items, setItems] = useState<MenuItem[]>([])
const [reviews, setReviews] = useState<Review[]>([])
const [navBarHeight, setNavBarHeight] = useState({ statusBarHeight: 20, navBarHeight: 44, totalHeight: 64 })
// === UI 状态 ===
const [activeTab, setActiveTab] = useState(0)
const [activeCategoryIndex, setActiveCategoryIndex] = useState(0)
const [cart, setCart] = useState<Record<string, number>>({})
const [cartSheetVisible, setCartSheetVisible] = useState(false)
/** 加载数据 */
useEffect(() => {
const id = String(Taro.getCurrentInstance().router?.params?.id || '')
if (!id) {
Taro.showToast({ title: '参数错误', icon: 'none' })
Taro.navigateBack()
return
}
const found = getShopById(id)
if (!found) {
Taro.showToast({ title: '商家不存在', icon: 'none' })
Taro.navigateBack()
return
}
setShop(found)
setCategories(getMenuCategories(id))
setItems(getMenuItems(id))
setReviews(getShopReviews(id))
setNavBarHeight(getNavBarHeight())
// 动态设置导航栏标题
Taro.setNavigationBarTitle({ title: found.name })
}, [])
// === 购物车派生数据 ===
const cartCount = useMemo(
() => Object.values(cart).reduce((sum, n) => sum + n, 0),
[cart],
)
const cartTotal = useMemo(() => {
return Object.entries(cart).reduce((sum, [itemId, qty]) => {
const item = items.find((i) => i.id === itemId)
return sum + (item ? item.price * qty : 0)
}, 0)
}, [cart, items])
const cartItems = useMemo(
() =>
Object.entries(cart)
.filter(([, qty]) => qty > 0)
.map(([itemId, qty]) => ({
item: items.find((i) => i.id === itemId)!,
qty,
}))
.filter((c) => c.item),
[cart, items],
)
// === 菜单按分类分组(保持原始顺序) ===
const groupedItems = useMemo(() => {
return categories.map((cat) => ({
category: cat,
list: items.filter((i) => i.categoryId === cat.id),
}))
}, [categories, items])
// === 加购 / 减购 ===
const handleAdd = (itemId: string) => {
setCart((prev) => ({ ...prev, [itemId]: (prev[itemId] || 0) + 1 }))
}
const handleSub = (itemId: string) => {
setCart((prev) => {
const next = { ...prev }
if (!next[itemId] || next[itemId] <= 1) {
delete next[itemId]
} else {
next[itemId] -= 1
}
return next
})
}
const handleClearCart = () => {
setCart({})
}
const handleSubmit = () => {
Taro.showToast({ title: `已提交 ${cartCount} 件,¥${cartTotal.toFixed(1)}`, icon: 'success' })
setCart({})
setCartSheetVisible(false)
}
const handleCartIconClick = () => {
if (cartCount === 0) {
Taro.showToast({ title: '还没有选购商品', icon: 'none' })
return
}
setCartSheetVisible(true)
}
// === 渲染前等待 shop 加载 ===
if (!shop) {
return <View className='shop-detail-page' />
}
return (
<View className='shop-detail-page'>
{/* ========== 自定义导航栏 ========== */}
<View
className='custom-navbar'
style={{ paddingTop: `${navBarHeight.statusBarHeight}px` }}
>
<View
className='navbar-content'
style={{ height: `${navBarHeight.navBarHeight}px` }}
>
<View className='navbar-back' onClick={() => Taro.navigateBack()}>
<Text className='back-icon'></Text>
</View>
<View className='navbar-title-wrap'>
<Text className='navbar-title' numberOfLines={1}>
{shop.name}
</Text>
</View>
<View className='navbar-action'>
<Text className='action-icon'></Text>
</View>
</View>
</View>
<View style={{ height: `${navBarHeight.totalHeight}px` }} />
{/* ========== 店铺头(背景图 + logo + 信息) ========== */}
<View className='shop-header'>
<View className='header-bg'>
<VantImage
className='header-bg-image'
src={shop.image}
fit='cover'
width='100%'
height='100%'
/>
<View className='header-bg-mask' />
</View>
<View className='header-content'>
<View className='header-logo-wrap'>
<VantImage
className='header-logo'
src={shop.logo}
fit='cover'
width='100%'
height='100%'
round
/>
</View>
<View className='header-info'>
<View className='header-name-row'>
<Text className='header-name'>{shop.name}</Text>
{shop.tags.map((t) => (
<Tag key={t} type='danger' plain size='medium' className='header-tag'>
{t}
</Tag>
))}
</View>
<View className='header-rating-row'>
<Rate
value={shop.rating}
readonly
size={14}
color='#ff6034'
voidColor='#dcdee0'
/>
<Text className='rating-value'>{shop.rating.toFixed(1)}</Text>
<Text className='header-sep'>·</Text>
<Text className='sales'> {shop.monthlySales}</Text>
</View>
<View className='header-fee-row'>
<Text className='fee-item'> ¥{shop.minOrder}</Text>
<Text className='fee-sep'>|</Text>
<Text className='fee-item'> ¥{shop.deliveryFee}</Text>
<Text className='fee-sep'>|</Text>
<Text className='fee-item'>{shop.deliveryTime}</Text>
</View>
</View>
</View>
{/* 公告 */}
{shop.announcements && (
<View className='header-announcement'>
<Text className='announcement-label'></Text>
<Text className='announcement-text' numberOfLines={2}>
{shop.announcements}
</Text>
</View>
)}
</View>
{/* ========== Tabs: 点菜 / 评价 / 商家 ========== */}
<Tabs
active={activeTab}
onChange={(e) => setActiveTab(e.detail.index)}
sticky
offsetTop={navBarHeight.totalHeight}
color='#1989fa'
titleActiveColor='#1989fa'
titleInactiveColor='#646566'
animated
>
<Tab title='点菜' />
<Tab title={`评价(${reviews.length})`} />
<Tab title='商家' />
</Tabs>
{/* ===== Tab 0: 点菜 ===== */}
{activeTab === 0 && (
<View className='menu-tab'>
<View className='menu-body'>
{/* 左侧 Sidebar */}
<Sidebar
activeKey={activeCategoryIndex}
onChange={(e) => setActiveCategoryIndex(e.detail ?? 0)}
className='menu-sidebar'
>
{groupedItems.map((g, idx) => (
<SidebarItem
key={g.category.id}
title={`${g.category.name} (${g.list.length})`}
/>
))}
</Sidebar>
{/* 右侧菜单 */}
<ScrollView className='menu-list-scroll' scrollY enhanced showScrollbar={false}>
{groupedItems.map((g) => (
<View key={g.category.id} className='menu-category'>
<View className='category-header'>
<Text className='category-name'>{g.category.name}</Text>
<Text className='category-count'> {g.list.length} </Text>
</View>
{g.list.map((item) => {
const qty = cart[item.id] || 0
return (
<View key={item.id} className='menu-item'>
<View className='menu-item-image-wrap'>
<VantImage
className='menu-item-image'
src={item.image}
fit='cover'
width='100%'
height='100%'
radius={8}
/>
</View>
<View className='menu-item-info'>
<View className='menu-item-name-row'>
<Text className='menu-item-name' numberOfLines={1}>
{item.name}
</Text>
{item.tags?.map((t) => (
<Tag
key={t}
type='danger'
plain
size='medium'
className='menu-item-tag'
>
{t}
</Tag>
))}
</View>
<Text className='menu-item-desc' numberOfLines={1}>
{item.description}
</Text>
<Text className='menu-item-sales'> {item.sales}</Text>
<View className='menu-item-bottom'>
<View className='price-row'>
<Text className='menu-item-price'>
¥{item.price.toFixed(1)}
</Text>
{item.originalPrice && (
<Text className='menu-item-original'>
¥{item.originalPrice.toFixed(1)}
</Text>
)}
</View>
<View className='stepper-row'>
{qty > 0 && (
<View
className='stepper-btn minus'
onClick={() => handleSub(item.id)}
>
<Text className='stepper-btn-text'></Text>
</View>
)}
{qty > 0 && (
<Text className='stepper-qty'>{qty}</Text>
)}
<View
className='stepper-btn plus'
onClick={() => handleAdd(item.id)}
>
<Text className='stepper-btn-text plus'>+</Text>
</View>
</View>
</View>
</View>
</View>
)
})}
</View>
))}
<View className='bottom-safe' />
</ScrollView>
</View>
</View>
)}
{/* ===== Tab 1: 评价 ===== */}
{activeTab === 1 && (
<View className='reviews-tab'>
<ScrollView scrollY enhanced showScrollbar={false} className='reviews-scroll'>
<View className='reviews-summary'>
<View className='summary-rating'>
<Text className='summary-rating-value'>{shop.rating.toFixed(1)}</Text>
<Text className='summary-rating-label'></Text>
</View>
<View className='summary-stats'>
<View className='stat-item'>
<Text className='stat-num'>{shop.monthlySales}</Text>
<Text className='stat-label'></Text>
</View>
<View className='stat-item'>
<Text className='stat-num'>{shop.deliveryTime}</Text>
<Text className='stat-label'></Text>
</View>
<View className='stat-item'>
<Text className='stat-num'>{reviews.length}</Text>
<Text className='stat-label'></Text>
</View>
</View>
</View>
<View className='reviews-list'>
{reviews.length === 0 && (
<View className='empty-tip'></View>
)}
{reviews.map((r) => (
<View key={r.id} className='review-card'>
<View className='review-header'>
<VantImage
className='review-avatar'
src={r.author.avatar}
fit='cover'
width='100%'
height='100%'
round
/>
<View className='review-meta'>
<Text className='review-name'>{r.author.name}</Text>
<View className='review-rate-row'>
<Rate
value={r.rating}
readonly
size={12}
color='#ff6034'
voidColor='#dcdee0'
/>
<Text className='review-time'>{r.time}</Text>
</View>
</View>
</View>
<Text className='review-content'>{r.content}</Text>
{r.images && r.images.length > 0 && (
<View className='review-images'>
{r.images.map((img, idx) => (
<View key={idx} className='review-image-wrap'>
<VantImage
className='review-image'
src={img}
fit='cover'
width='100%'
height='100%'
radius={6}
/>
</View>
))}
</View>
)}
{r.dishes && r.dishes.length > 0 && (
<View className='review-dishes'>
{r.dishes.map((d) => (
<Tag
key={d}
type='primary'
plain
size='medium'
className='review-dish-tag'
>
{d}
</Tag>
))}
</View>
)}
</View>
))}
</View>
<View className='bottom-safe' />
</ScrollView>
</View>
)}
{/* ===== Tab 2: 商家 ===== */}
{activeTab === 2 && (
<View className='merchant-tab'>
<ScrollView scrollY enhanced showScrollbar={false} className='merchant-scroll'>
{/* 商家相册 */}
{shop.photos.length > 0 && (
<View className='merchant-section'>
<Text className='merchant-section-title'></Text>
<View className='merchant-photos'>
{shop.photos.map((p, idx) => (
<View key={idx} className='merchant-photo-wrap'>
<VantImage
className='merchant-photo'
src={p}
fit='cover'
width='100%'
height='100%'
radius={8}
/>
</View>
))}
</View>
</View>
)}
{/* 商家标签 */}
{shop.tags.length > 0 && (
<View className='merchant-section'>
<Text className='merchant-section-title'></Text>
<View className='merchant-tags'>
{shop.tags.map((t) => (
<Tag key={t} type='primary' plain size='medium' className='merchant-tag'>
{t}
</Tag>
))}
</View>
</View>
)}
{/* 商家信息 */}
<View className='merchant-section'>
<Text className='merchant-section-title'></Text>
<View className='merchant-info-list'>
<View className='merchant-info-row'>
<Text className='info-label'></Text>
<Text className='info-value'>{shop.address}</Text>
</View>
<View className='merchant-info-row'>
<Text className='info-label'></Text>
<Text className='info-value'>{shop.phone}</Text>
</View>
<View className='merchant-info-row'>
<Text className='info-label'></Text>
<Text className='info-value'>{shop.businessHours}</Text>
</View>
<View className='merchant-info-row'>
<Text className='info-label'></Text>
<Text className='info-value'>{shop.deliveryHours}</Text>
</View>
</View>
</View>
{/* 服务设施 */}
{shop.services.length > 0 && (
<View className='merchant-section'>
<Text className='merchant-section-title'></Text>
<View className='merchant-tags'>
{shop.services.map((s) => (
<Tag key={s} type='success' plain size='medium' className='merchant-tag'>
{s}
</Tag>
))}
</View>
</View>
)}
{/* 公告 */}
{shop.announcements && (
<View className='merchant-section'>
<Text className='merchant-section-title'></Text>
<View className='merchant-announcement'>
<Text className='announcement-text'>{shop.announcements}</Text>
</View>
</View>
)}
<View className='bottom-safe' />
</ScrollView>
</View>
)}
{/* ========== 底部购物车 SubmitBar(仅点菜 Tab 显示)========== */}
{activeTab === 0 && (
<View className='submit-bar-wrap'>
<View className='cart-icon-btn' onClick={handleCartIconClick}>
<Text className='cart-icon'>🛒</Text>
{cartCount > 0 && (
<View className='cart-badge'>
<Text className='cart-badge-text'>{cartCount}</Text>
</View>
)}
</View>
<View className='cart-info'>
<Text className='cart-price'>
¥{cartTotal.toFixed(1)}
</Text>
<Text className='cart-fee'>
¥{shop.deliveryFee}
</Text>
</View>
<View
className={`submit-btn ${cartCount > 0 && cartTotal >= shop.minOrder ? 'active' : 'disabled'}`}
onClick={cartCount > 0 ? handleSubmit : undefined}
>
<Text className='submit-btn-text'>
{cartCount === 0
? `¥${shop.minOrder} 起送`
: cartTotal < shop.minOrder
? `还差 ¥${(shop.minOrder - cartTotal).toFixed(1)}`
: '去结算'}
</Text>
</View>
</View>
)}
{/* ========== 购物车 Popup ========== */}
<Popup
position='bottom'
round
show={cartSheetVisible}
onClose={() => setCartSheetVisible(false)}
closeOnClickOverlay
safeAreaInsetBottom
>
<View className='cart-sheet'>
<View className='cart-sheet-header'>
<Text className='cart-sheet-title'></Text>
<View className='cart-clear' onClick={handleClearCart}>
<Text className='cart-clear-text'>🗑 </Text>
</View>
</View>
<ScrollView scrollY enhanced showScrollbar={false} className='cart-sheet-list'>
{cartItems.map(({ item, qty }) => (
<View key={item.id} className='cart-row'>
<View className='cart-row-image-wrap'>
<VantImage
className='cart-row-image'
src={item.image}
fit='cover'
width='100%'
height='100%'
radius={6}
/>
</View>
<View className='cart-row-info'>
<Text className='cart-row-name' numberOfLines={1}>
{item.name}
</Text>
<Text className='cart-row-price'>¥{item.price.toFixed(1)}</Text>
</View>
<Stepper
value={qty}
min={0}
theme='round'
onPlus={() => handleAdd(item.id)}
onMinus={() => handleSub(item.id)}
/>
</View>
))}
{cartItems.length === 0 && (
<View className='cart-sheet-empty'></View>
)}
</ScrollView>
<View className='cart-sheet-footer'>
<View className='cart-sheet-total'>
<Text className='cart-sheet-total-label'></Text>
<Text className='cart-sheet-total-price'>¥{cartTotal.toFixed(1)}</Text>
<Text className='cart-sheet-total-fee'>
¥{shop.deliveryFee}
</Text>
</View>
<View
className={`cart-sheet-submit ${cartCount > 0 ? 'active' : 'disabled'}`}
onClick={cartCount > 0 ? handleSubmit : undefined}
>
<Text className='cart-sheet-submit-text'></Text>
</View>
</View>
</View>
</Popup>
</View>
)
}
+17 -1
View File
@@ -2,7 +2,15 @@
export type ArticleCardType = 'text' | 'image-text' | 'image-grid'
/** 文章分类 */
export type ArticleCategory = 'latest' | 'recommend' | 'hot'
export type ArticleCategory = 'latest' | 'recommend' | 'hot' | 'forum'
/** 论坛帖子子分类(仅当 category='forum' 时使用) */
export type ForumSubCategory =
| 'life' // 校园生活
| 'study' // 学习交流
| 'lost-found' // 失物招领
| 'secondhand' // 二手交易
| 'tree-hole' // 树洞
/** 文章数据接口 */
export interface Article {
@@ -15,6 +23,14 @@ export interface Article {
images: string[]
type: ArticleCardType
category: ArticleCategory
/** 仅论坛帖子使用 */
subCategory?: ForumSubCategory
/** 评论数(论坛帖子会用到) */
commentCount?: number
/** 作者头像 URL(仅论坛帖子使用) */
sourceAvatar?: string
/** 点赞数(仅论坛帖子使用) */
likeCount?: number
}
/** 导航项 */
+76
View File
@@ -0,0 +1,76 @@
/** 商家分类(校园外卖首页导航组) */
export type ShopCategory = 'food' | 'dessert' | 'supermarket' | 'medicine' | 'fruit'
/** 商家信息 */
export interface Shop {
id: string
name: string
/** 商家主图(卡片左侧、店铺头背景) */
image: string
/** 圆形 logo */
logo: string
/** 评分 0-5 */
rating: number
/** 月售 */
monthlySales: number
/** 配送时间(分钟) */
deliveryTime: number
/** 配送费(元) */
deliveryFee: number
/** 起送价(元) */
minOrder: number
/** 距离(米)字符串 */
distance: string
category: ShopCategory
/** 标签如"满减"、"新店" */
tags: string[]
/** 商家介绍(卡片右侧文案) */
description: string
/** 是否为首页推荐 */
recommended?: boolean
// === 商家 Tab 用 ===
address: string
phone: string
/** "08:00-22:00" */
businessHours: string
/** "立即配送" */
deliveryHours: string
/** ["可堂食","支持开发票",...] */
services: string[]
/** 商家相册 */
photos: string[]
/** 公告 */
announcements?: string
}
/** 菜单分类(左侧 Sidebar 一项) */
export interface MenuCategory {
id: string
name: string
}
/** 菜单商品 */
export interface MenuItem {
id: string
name: string
image: string
price: number
originalPrice?: number
sales: number
description: string
tags?: string[]
/** 关联 MenuCategory.id */
categoryId: string
}
/** 评价 */
export interface Review {
id: string
author: { name: string; avatar: string }
rating: number
content: string
images?: string[]
time: string
/** 点了什么菜 */
dishes?: string[]
}