Explorar el Código

视频列表页及详情页基础处理;

huan-jie hace 4 años
padre
commit
6766fe54ce

+ 403 - 0
assets/css/learn/detail/_id.scss

@@ -0,0 +1,403 @@
+.mobileMain {
+    position: relative;
+    width: 100%;
+    min-height: calc(100vh - 64px);
+    margin: 0 !important;
+}
+.mobileWeb {
+    margin-bottom: 30px !important;
+}
+.learn-detail-wrapper {
+    position: relative;
+    width: 1000px;
+    .learn-info {
+        width: 1000px;
+        height: 328px;
+        padding: 34px 30px;
+        opacity: 1;
+        background: #ffffff;
+        border-radius: 10px;
+        display: flex;
+        .cover {
+            flex-shrink: 0;
+            width: calc(224px * 16 / 9);
+            height: 224px;
+            overflow: hidden;
+            border-radius: 5px;
+            object-fit: cover;
+        }
+        .info-wrapper {
+            flex: 1;
+            margin-left: 20px;
+            margin-right: 20px;
+            display: flex;
+            flex-direction: column;
+            overflow: hidden;
+            .title {
+                width: 100%;
+                min-height: 33px;
+                line-height: 33px;
+                font-size: 24px;
+                font-family: PingFangSC, PingFangSC-Medium;
+                font-weight: 500;
+                color: #222222;
+                overflow: hidden;
+                text-overflow: ellipsis;
+                display: -webkit-box;
+                white-space: normal;
+                -webkit-box-orient: vertical;
+                -webkit-line-clamp: 2;
+            }
+            .owner-content {
+                margin-top: 8px;
+                height: 24px;
+                display: flex;
+                align-items: center;
+                .owner-info {
+                    cursor: pointer;
+                    display: flex;
+                    align-items: center;
+                    img {
+                        width: 24px;
+                        height: 24px;
+                        border-radius: 50%;
+                    }
+                    span {
+                        margin-left: 4px;
+                        font-size: 12px;
+                        font-family: PingFangSC, PingFangSC-Medium;
+                        font-weight: 500;
+                        color: #222222;
+                    }
+                }
+            }
+            .price-content {
+                height: 27px;
+                margin-top: 29px;
+                display: flex;
+                align-items: center;
+                .price-text {
+                    font-size: 23px;
+                    font-family: PingFangSC, PingFangSC-Semibold;
+                    font-weight: 600;
+                    color: #ff6600;
+                }
+                .buy-num {
+                    margin-left: 22px;
+                    font-size: 14px;
+                    font-family: PingFangSC, PingFangSC-Regular;
+                    font-weight: 400;
+                    color: rgba(153,153,153,1);
+                    span {
+                        color: #308eff;
+                    }
+                }
+            }
+            .actions {
+                margin-top: 16px;
+                height: 50px;
+                display: flex;
+                align-items: center;
+                .btn-1 {
+                    width: 152px;
+                    height: 50px;
+                    opacity: 1;
+                    background: #308eff;
+                    border-radius: 4px;
+                    border: none;
+                    font-size: 15px;
+                    font-family: PingFangSC, PingFangSC-Medium;
+                    font-weight: 500;
+                    color: #ffffff;
+                }
+                .btn-2 {
+                    width: 162px;
+                    height: 50px;
+                    margin-left: 10px;
+                    background: rgba(48,142,255,0.11);
+                    border-radius: 4px;
+                    border: none;
+                    font-size: 15px;
+                    font-family: PingFangSC, PingFangSC-Medium;
+                    font-weight: 500;
+                    color: #308eff;
+                    .btn-content {
+                        display: flex;
+                        align-items: center;
+                        justify-content: center;
+                        img {
+                            width: 18px;
+                            height: 18px;
+                            margin-right: 4px;
+                        }
+                    }
+                }
+            }
+        }
+        .qrcode-content {
+            flex-shrink: 0;
+            margin-top: 20px;
+            width: 160px;
+            height: 200px;
+            background: #ffffff;
+            border: 1px solid #eaeaea;
+            display: flex;
+            flex-direction: column;
+            align-items: center;
+            img {
+                width: 136px;
+                height: 136px;
+                margin-top: 15px;
+            }
+            .qrcode-tips {
+                height: 17px;
+                line-height: 17px;
+                font-size: 12px;
+                margin-top: 12px;
+                font-family: PingFangSC, PingFangSC-Regular;
+                font-weight: 400;
+                color: #333;
+                a {
+                    color: #308eff;
+                    text-decoration: underline;
+                }
+            }
+        }
+    }
+    .learn-content {
+        width: 100%;
+        height: auto;
+        padding: 24px 0 42px;
+        margin-top: 20px;
+        background: #ffffff;
+        border-radius: 10px;
+        .learn-image-list {
+            margin-top: 10px;
+            width: 100%;
+            padding: 0 32px;
+            display: flex;
+            flex-direction: column;
+            align-items: center;
+            img {
+                margin-top: 10px;
+                max-width: 100%;
+                object-fit: scale-down;
+                border-radius: 5px;
+            }
+        }
+        .content-text {
+            margin-top: 20px;
+            width: 100%;
+            padding: 0 32px;
+        }
+        .extra-wrapper {
+            margin: 22px 40px 0;
+            width: calc(100% - 80px);
+            height: 106px;
+            background: #f7f7f7;
+            border-radius: 12px;
+        }
+    }
+    .common-title {
+        position: relative;
+        width: 100%;
+        height: 40px;
+        line-height: 40px;  
+        padding-left: 32px;
+        font-size: 24px;
+        font-family: PingFangSC, PingFangSC-Medium;
+        font-weight: 500;
+        color: #222222;
+        &:after {
+            position: absolute;
+            content: " ";
+            width: 3px;
+            height: 20px;
+            opacity: 1;
+            background: #308eff;
+            top: 10px;
+            left: 0;
+        }
+    }
+    .contact-wrapper {
+        position: relative;
+        .contact-item {
+            display: flex;
+            align-items: center;
+            height: 22px;
+            margin-bottom: 20px;
+            img {
+                width: 22px;
+                height: 22px;
+            }
+            .contact-text {
+                font-size: 14px;
+                font-family: PingFangSC, PingFangSC-Medium;
+                font-weight: 500;
+                color: #333333;
+                margin-left: 10px;
+            }
+            &:last-of-type {
+                margin-bottom: 0;
+            }
+        }
+    }
+}
+.learn-detail-wrapper-mobile {
+    position: relative;
+    width: 100%;
+    padding-bottom: calc(56px + 8px + constant(safe-area-inset-bottom));
+    padding-bottom: calc(56px + 8px + env(safe-area-inset-bottom));
+    display: flex;
+    flex-direction: column;
+    .image-list {
+        width: 100%;
+        height: calc(100vw * 9 / 16);
+        .image-swiper {
+            position: relative;
+            width: 100%;
+            height: 100%;
+            display: flex;
+            justify-content: center;
+        }
+        .swiper-wrapper {
+            width: 100%;
+            height: 100%;
+            .swiper-slide {
+                width: 100%;
+                height: 100%;
+                img {
+                    width: 100%;
+                    height: 100%;
+                    object-fit: scale-down;
+                }
+            }
+        }
+        .swiper-pagination {
+            position: absolute;
+            bottom: 10px;
+            left: auto !important;
+        }
+    }
+    .learn-info {
+        width: 100%;
+        height: 105px;
+        padding: 15px 10px;
+        background: #ffffff;
+        border-radius: 10px 10px 0px 0px;
+        .learn-title {
+            width: 100%;
+            height: 25px;
+            line-height: 25px;
+            font-size: 18px;
+            font-family: PingFangSC, PingFangSC-Semibold;
+            font-weight: 600;
+            color: #222222;
+            overflow: hidden;
+            text-overflow: ellipsis;
+            white-space: nowrap;
+        }
+        .userinfo-wrapper {
+            margin-top: 15px;
+            width: 100%;
+            height: 34px;
+            display: flex;
+            align-items: center;
+            justify-content: space-between;
+            .left-info {
+                display: flex;
+                align-items: center;
+                .avatar {
+                    width: 34px;
+                    height: 34px;
+                    border-radius: 50%;
+                    overflow: hidden;
+                }
+                .nickname {
+                    margin-left: 9px;
+                    height: 20px;
+                    line-height: 20px;
+                    font-size: 14px;
+                    font-family: PingFangSC, PingFangSC-Medium;
+                    font-weight: 500;
+                    color: #222222;
+                }
+            }
+        }
+    }
+    .learn-content {
+        margin-top: 10px;
+        width: 100%;
+        background: #ffffff;
+        .ql-editor {
+            padding: 20px 12px 30px;
+        }
+    }
+    .pay-wrapper {
+        position: fixed;
+        bottom: 0;
+        left: 0;
+        width: 100%;
+        height: calc(56px + constant(safe-area-inset-bottom));
+        height: calc(56px + env(safe-area-inset-bottom));
+        padding: 0 10px;
+        background: #ffffff;
+        display: flex;
+        justify-content: space-between;
+        .price-info {
+            flex-shrink: 0;
+            margin-top: 10px;
+            display: flex;
+            flex-direction: column;
+            .price-text {
+                height: 18px;
+                line-height: 18px;
+                font-size: 19px;
+                font-family: PingFangSC, PingFangSC-Semibold;
+                font-weight: 600;
+                color: #ff6600;
+            }
+            span {
+                height: 18px;
+                line-height: 18px;
+                font-size: 12px;
+                font-family: PingFangSC, PingFangSC-Regular;
+                font-weight: 400;
+                color: #999999;
+            }
+        }
+        .pay-btn {
+            flex-shrink: 0;
+            margin-top: 6px;
+            width: 122px;
+            height: 44px;
+            background: #308eff;
+            border-radius: 6px;
+            font-size: 15px;
+            font-family: PingFangSC, PingFangSC-Medium;
+            font-weight: 500;
+            color: #ffffff;
+        }
+        .chat-content {
+            flex: 1;
+            display: flex;
+            justify-content: flex-end;
+            align-items: center;
+            .chat-btn {
+                display: flex;
+                align-items: center;
+                .chat-icon {
+                    width: 24px;
+                    height: 24px;
+                    margin-right: 4px;
+                }
+                .chat-word {
+                    font-size: 13px;
+                    font-weight: 400;
+                    color: rgba(23, 34, 47, 1);
+                    margin-right: 12px;
+                }
+            }
+        }
+    }
+}

+ 487 - 0
assets/css/learn/list.scss

@@ -0,0 +1,487 @@
+.learn-wrapper {
+    width: 1000px;
+    .learn-top {
+        width: 100%;
+        height: auto;
+        background: #ffffff;
+        border-radius: 10px;
+        .tabs {
+            position: relative;
+            width: 100%;
+            height: 50px;
+            display: flex;
+            align-items: center;
+            border-bottom: 1px solid #f1f1f1;
+            .add-btn {
+                position: absolute;
+                right: 20px;
+                width: 96px;
+                height: 32px;
+                padding: 0;
+                border: none;
+                background: #308eff;
+                border-radius: 5px;
+                font-size: 11px;
+                font-family: PingFangSC, PingFangSC-Medium;
+                font-weight: 500;
+                text-align: center;
+                color: #ffffff;
+            }
+            .tabs-item {
+                position: relative;
+                width: 108px;
+                height: 50px;
+                line-height: 50px;
+                font-size: 16px;
+                font-family: PingFangSC, PingFangSC-Regular;
+                font-weight: 400;
+                text-align: center;
+                color: #666666;
+                &.active {
+                    font-size: 15px;
+                    font-family: PingFangSC, PingFangSC-Semibold;
+                    font-weight: 600;
+                    color: #222222;
+                    &:after {
+                        position: absolute;
+                        content: " ";
+                        width: 15px;
+                        height: 3px;
+                        bottom: 1px;
+                        left: calc(50% - 7.5px);
+                        background: #308eff;
+                        border-radius: 2px;
+                    }
+                }
+            }
+        }
+    }
+    .learn-category-wrapper {
+        width: 100%;
+        height: auto;
+        padding: 15px 20px;
+        .category-title {
+            width: 100%;
+            line-height: 21px;
+            padding-bottom: 7px;
+            font-size: 15px;
+            font-family: PingFangSC, PingFangSC-Semibold;
+            font-weight: 600;
+            color: #222222;
+        }
+        .category-one-wrapper {
+            position: relative;
+            height: 40px;
+            width: 100%;
+            padding-right: 60px;
+            overflow: hidden;
+            display: flex;
+            flex-wrap: wrap;
+            transition: all .3s;
+            .category-more {
+                position: absolute;
+                height: 30px;
+                line-height: 30px;
+                margin-top: 10px;
+                padding: 0 15px;
+                right: 0;
+                top: 0;
+                font-size: 13px;
+                color: #999999;
+                cursor: pointer;
+            }
+            &.expand {
+                height: auto !important;
+            }
+            .category-one-item {
+                height: 30px;
+                line-height: 30px;
+                padding: 0 25px;
+                margin-top: 10px;
+                border-radius: 4px;
+                font-size: 14px;
+                font-family: PingFangSC, PingFangSC-Regular;
+                font-weight: 400;
+                text-align: center;
+                color: #555555;
+                cursor: pointer;
+                &.active {
+                    line-height: 28px;
+                    font-family: PingFangSC, PingFangSC-Medium;
+                    font-weight: 500;
+                    color: #308eff;
+                    border: 1px solid #308eff;
+                }
+                &:hover {
+                    color: #308eff;
+                }
+            }
+        }
+        .category-two-wrapper {
+            position: relative;
+            margin-top: 16px;
+            .category-two-content {
+                position: relative;
+                width: 830px;
+                height: auto;
+                padding: 5px 20px 10px;
+                // margin-left: 80px;
+                background: #f7f7f7;
+                border-radius: 8px;
+                display: flex;
+                flex-wrap: wrap;
+                .category-two-item {
+                    height: 30px;
+                    line-height: 30px;
+                    margin-top: 5px;
+                    padding: 0 15px;
+                    color: #555555;
+                    border-radius: 4px;
+                    font-size: 14px;
+                    cursor: pointer;
+                    &.active {
+                        color: #308eff;
+                        background: #dcefff;
+                    }
+                    &:hover {
+                        color: #308eff;
+                    }
+                }
+            }
+        }
+    }
+    .learn-content {
+        width: 100%;
+        margin-top: 10px;
+        padding: 12px 20px 36px;
+        background: #ffffff;
+        border-radius: 10px;
+        .learn-list-wrapper {
+            position: relative;
+            width: 100%;
+            display: flex;
+            flex-wrap: wrap;
+            .learn-item {
+                width: 310px;
+                height: 310px;
+                margin-right: 15px;
+                margin-bottom: 15px;
+                background: #ffffff;
+                border-radius: 10px;
+                overflow: hidden;
+                box-shadow: 0px 6px 16px 0px rgba(6,10,28,0.06); 
+                display: flex;
+                flex-direction: column;
+                // cursor: pointer;
+                &:nth-of-type(3n) {
+                    margin-right: 0;
+                }
+                .cover {
+                    width: 100%;
+                    height: 175px;
+                    overflow: hidden;
+                    object-fit: scale-down;
+                }
+                .owner-wrapper {
+                    width: 100%;
+                    height: 22px;
+                    margin-top: 10px;
+                    padding: 0 10px;
+                    display: flex;
+                    align-items: center;
+                    .avatar {
+                        width: 22px;
+                        height: 22px;
+                        overflow: hidden;
+                        border-radius: 50%;
+                    }
+                    .nickname {
+                        line-height: 22px;
+                        margin-left: 6px;
+                        font-size: 14px;
+                        font-family: PingFangSC, PingFangSC-Regular;
+                        font-weight: 400;
+                        color: #666666;
+                    }
+                }
+                .title {
+                    width: 100%;
+                    height: 42px;
+                    margin-top: 12px;
+                    padding: 0 12px;
+                    line-height: 21px;
+                    font-size: 15px;
+                    font-family: PingFangSC, PingFangSC-Medium;
+                    font-weight: 500;
+                    color: #333333;
+                    overflow: hidden;
+                    text-overflow: ellipsis;
+                    display: -webkit-box;
+                    white-space: normal;
+                    -webkit-box-orient: vertical;
+                    -webkit-line-clamp: 2;
+                    &:hover {
+                        color: #308eff;
+                    }
+                }
+                .price-wrapper {
+                    width: 100%;
+                    height: 20px;
+                    margin-top: 12px;
+                    padding: 0 12px;
+                    display: flex;
+                    align-items: center;
+                    justify-content: space-between;
+                    .price-text {
+                        line-height: 20px;
+                        font-size: 19px;
+                        font-family: DINAlternate, DINAlternate-Bold;
+                        font-weight: 700;
+                        color: #ff6600;
+                    }
+                    .buy-num {
+                        font-size: 14px;
+                        font-family: PingFangSC, PingFangSC-Regular;
+                        font-weight: 400;
+                        color: #999999;
+                    }
+                }
+            }
+        }
+        .pagination-wrapper {
+            width: 100%;
+            height: 30px;
+            margin-top: 25px;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+        }
+    }
+}
+.mobileMain {
+    width: 100%;
+    margin: 0;
+}
+.learn-wrapper-mobile {
+    position: relative;
+    width: 100%;
+    // min-height: 100vh;
+    display: flex;
+    flex-direction: column;
+    .learn-category {
+        position: fixed;
+        width: 100%;
+        height: 87px;
+        z-index: 11;
+        .learn-category-one {
+            position: relative;
+            width: 100%;
+            height: 45px;
+            background: #ffffff;
+            overflow: hidden;
+            .category-scroller {
+                width: calc(100% - 64px);
+                overflow-x: scroll;
+                overflow-y: hidden;
+                -webkit-overflow-scrolling: touch;
+                display: flex;
+                &::-webkit-scrollbar {
+                    display: none;
+                }
+            }
+            .learn-category-one-item {
+                position: relative;
+                flex-shrink: 0;
+                width: 76px;
+                height: 45px;
+                line-height: 45px;
+                font-size: 16px;
+                font-family: PingFangSC, PingFangSC-Regular;
+                font-weight: 400;
+                color: #666666;
+                font-size: 15px;
+                text-align: center;
+                &.active {
+                    font-family: PingFangSC, PingFangSC-Semibold;
+                    font-weight: 600;
+                    color: #222222;
+                    &:after {
+                        position: absolute;
+                        content: " ";
+                        bottom: 0;
+                        left: calc(50% - 7.5px);
+                        width: 15px;
+                        height: 3px;
+                        background: #308eff;
+                        border-radius: 2px;
+                    }
+                }
+            }
+            .filter-wrapper {
+                position: absolute;
+                top: 0;
+                right: 0;
+                width: 64px;
+                height: 45px;
+                display: flex;
+                align-items: center;
+                justify-content: center;
+                z-index: 12;
+                background: #ffffff;
+                img {
+                    width: 24px;
+                    height: 24px;
+                }
+            }
+            .filter-bg {
+                position: absolute;
+                width: 11px;
+                height: 29px;
+                right: 59px;
+                top: 8px;
+                opacity: 0.1;
+                background: #000000;
+                border-radius: 6px;
+                filter: blur(4px);
+                z-index: 11;
+            }
+        }
+        .learn-category-two {
+            position: relative;
+            width: 100%;
+            height: 42px;
+            background: #ffffff;
+            .learn-category-two-wrapper {
+                position: relative;
+                width: 100%;
+                height: 42px;
+                padding: 0 20px;
+                overflow-x: scroll;
+                overflow-y: hidden;
+                -webkit-overflow-scrolling: touch;
+                display: flex;
+                &::-webkit-scrollbar {
+                    display: none;
+                }
+                .learn-category-two-item {
+                    position: relative;
+                    flex-shrink: 0;
+                    height: 42px;
+                    line-height: 42px;
+                    margin-right: 20px;
+                    font-size: 14px;
+                    font-family: PingFangSC, PingFangSC-Regular;
+                    font-weight: 400;
+                    color: #666666;
+                    &:last-child {
+                        margin-right: 0;
+                    }
+                    &.active {
+                        color: #308eff;
+                    }
+                }
+            }
+        }
+    }
+    .learn-list {
+        width: 100%;
+        height: 100vh;
+        padding-top: 87px;
+        padding-bottom: 34px;
+        overflow-x: hidden;
+        overflow-y: auto;
+        -webkit-overflow-scrolling: touch;
+        display: flex;
+        flex-direction: column;
+        align-items: center;
+        .learn-list-wrapper {
+            width: 100%;
+            height: auto;
+            overflow: visible;
+            .learn-item {
+                width: calc(100% - 20px);
+                height: auto;
+                margin: 10px auto 0;
+                padding-bottom: 16px;
+                background: #ffffff;
+                overflow: hidden;
+                border-radius: 10px;
+                box-shadow: 0px 6px 16px 0px rgba(6,10,28,0.06); 
+                display: flex;
+                flex-direction: column;
+                .cover {
+                    width: 100%;
+                    height: calc((100vw - 20px) * 9 / 16);
+                    overflow: hidden;
+                    object-fit: cover;
+                }
+                .owner-wrapper {
+                    width: 100%;
+                    height: 22px;
+                    margin-top: 10px;
+                    padding: 0 10px;
+                    display: flex;
+                    align-items: center;
+                    .avatar {
+                        width: 22px;
+                        height: 22px;
+                        overflow: hidden;
+                        border-radius: 50%;
+                    }
+                    .nickname {
+                        line-height: 22px;
+                        margin-left: 6px;
+                        font-size: 14px;
+                        font-family: PingFangSC, PingFangSC-Regular;
+                        font-weight: 400;
+                        color: #666666;
+                    }
+                }
+                .title {
+                    width: 100%;
+                    height: 20px;
+                    margin-top: 12px;
+                    padding: 0 12px;
+                    line-height: 20px;
+                    font-size: 15px;
+                    font-family: PingFangSC, PingFangSC-Medium;
+                    font-weight: 500;
+                    color: #333333;
+                    overflow: hidden;
+                    text-overflow: ellipsis;
+                    white-space: nowrap;
+                }
+                .price-wrapper {
+                    width: 100%;
+                    height: 20px;
+                    margin-top: 16px;
+                    padding: 0 12px;
+                    display: flex;
+                    align-items: center;
+                    justify-content: space-between;
+                    .price-text {
+                        line-height: 20px;
+                        font-size: 19px;
+                        font-family: DINAlternate, DINAlternate-Bold;
+                        font-weight: 700;
+                        color: #ff6600;
+                    }
+                    .right-info {
+                        font-size: 14px;
+                        font-family: PingFangSC, PingFangSC-Regular;
+                        font-weight: 400;
+                        color: #999999;
+                    }
+                }
+            }
+        }
+        .learn-list-tips {
+            margin-top: 16px;
+            width: 100%;
+            text-align: center;
+        }
+    }
+    .learn-list__showWxHeader {
+        height: calc(100vh - 64px);
+    }
+}

+ 62 - 0
components/learn/dealSeoDetail.js

@@ -0,0 +1,62 @@
+export default class DealSeoData {
+    constructor({$axios, req, app, redirect, error}) {
+        this.$axios = $axios
+        this.req = req
+        this.app = app
+        this.redirect = redirect
+        this.error = error
+        this.skillDetail = {}
+        this.isExist = true
+    }
+
+    async dealData() {
+        let {
+            name,
+            query: { act },
+            path,
+            params,
+            fullPath
+        } = this.app.context.route
+        const sale_id = params.id || ''
+
+        // 重定向
+        if (path.indexOf('/frontend/learn/detail') > -1) {
+            this.redirect(301, '/l/' + sale_id)
+        }
+
+        const skillDetail = {}
+
+        return {
+            isExist: this.isExist,
+            sale_id,
+            skillDetail,
+            mobile: this.app.$deviceType.isMobile(),
+            head: this.dealThisMeta(),
+            act
+        }
+    }
+
+    dealThisMeta() {
+        if (!this.isExist) {
+            // 页面不存在时
+            return {
+                title: "页面不存在-程序员客栈",
+                keyword: "",
+                description: "",
+                h1: "",
+                canonical: "",
+                metaLocation: ""
+            }
+        }
+
+        let head = {
+            title: `客栈学院`,
+            keyword: `客栈学院`,
+            description: `客栈学院`,
+            h1: "",
+            canonical: "",
+            metaLocation: ""
+        }
+        return head
+    }
+}

+ 200 - 0
components/learn/dealSeoList.js

@@ -0,0 +1,200 @@
+export default class DealSeoData {
+    constructor({$axios, req, app, redirect, error}) {
+        this.$axios = $axios
+        this.req = req
+        this.app = app
+        this.redirect = redirect
+        this.error = error
+        this.pagination = {
+            page: 1,
+            pagesize: 9,
+            total: 0,
+            loading: false,
+            selectedCateIdOne: '',
+            selectedCateIdTwo: '',
+            noMore: true
+        }
+        this.cateNameOne = ''
+        this.cateNameTwo = ''
+        this.mobile = this.app.$deviceType.isMobile()
+        this.root_type = 0
+    }
+
+    async dealData() {
+        const self = this
+        let {
+            name,
+            query: { page = 1, root_type = 0 },
+            path,
+            params,
+            fullPath
+        } = this.app.context.route
+        this.pagination.page = Number(page)
+        this.root_type = root_type
+
+        // 目前仅将二级 id 拼接到 url 上
+        let match = params.pathMatch || ''
+        let matchList = match.split('/')
+        matchList.pop()
+        let lastMatch = matchList.pop()
+        // console.log(`match: ${match}, matchList: ${matchList}, lastMatch: ${lastMatch}`)
+
+        // 重定向
+        if (path.indexOf('/frontend/learn/list') > -1) {
+            this.redirect(301, '/learn/' + lastMatch)
+        }
+
+        let learnCate = await this._getSkillCate()
+        let learnCateAll = []
+        learnCate.forEach(item => {
+            if (item.children && item.children.length) {
+                item.children.forEach(child => {
+                    learnCateAll.push(child)
+                })
+            }
+        })
+
+        if (lastMatch) {
+            // 遍历分类数组,因为每个 children 都添加了“全部”,此处逻辑无需修改
+            let selectedCateIdOne = ''
+            learnCate.forEach(cateOne => {
+                cateOne.children.forEach(cateTwo => {
+                    if (cateTwo.value === lastMatch) {
+                        selectedCateIdOne = cateOne.value
+                        self.cateNameOne = cateOne.label
+                        if (cateTwo.label === '全部') {
+                            self.cateNameTwo = cateOne.label
+                        } else {
+                            self.cateNameTwo = cateTwo.label
+                        }
+                    }
+                })
+            })
+            if (selectedCateIdOne) {
+                this.pagination.selectedCateIdOne = selectedCateIdOne
+                this.pagination.selectedCateIdTwo = lastMatch
+            }
+        }
+
+        // 处理完分类信息,再获取数据
+        let learnList = await this._getLearnList()
+
+        return {
+            root_type,
+            learnCate,
+            learnCateAll,
+            learnList, //首次获取的数据
+            mobile: this.mobile,
+            pagination: this.pagination,
+            head: this.dealThisMeta()
+        }
+    }
+
+    /** 获取技能分类 */
+    async _getSkillCate () {
+        let res = await this.$axios.$post('/api/sale/cateListYes', { type: 2, root_type: this.root_type })
+        let learnCate = []
+
+        if (Number(res.status) === 1) {
+            learnCate = res.data || []
+            learnCate = learnCate.map(item => {
+                let children = item.child_list.map(child => {
+                    return {
+                        value: child.f_name,
+                        label: child.name
+                    }
+                })
+                return {
+                    value: item.f_name,
+                    label: item.name,
+                    children: children
+                }
+            })
+
+            // web 端,为所有二级分类添加 “全部”
+            if (!this.mobile) {
+                learnCate.forEach(item => {
+                    if (item.children) {
+                        let allItem = { value: item.value, label: '全部' }
+                        item.children.splice(0, 0, allItem)
+                    }
+                })
+            }
+        }
+
+        return learnCate
+    }
+
+    /** 获取技能服务列表 */
+    async _getLearnList () {
+        // 接口参数释义:https://www.yesdev.cn/apidocs-detail-20.html
+        const data = {
+            type: 4,
+            page: this.pagination.page,
+            page_size: this.pagination.pagesize,
+            cate_id: this.pagination.selectedCateIdTwo,
+            status: 2,
+            owner_type: 1,
+            root_type: this.root_type
+        }
+
+        let res = await this.$axios.$post('/api/sale/saleList', data)
+        let learnList = []
+
+        if (Number(res.status) === 1) {
+            learnList = res.data.list || []
+            learnList.forEach((item) => {
+                let imageList = item.image.split(',')
+                item.coverImage = imageList[0] || ''
+                imageList.splice(0, 1)
+                item.imageList = imageList
+            })
+            this.pagination.total = Number(res.data.total)
+            this.pagination.pagesize = Number(res.data.page_size) || 9
+
+            if (this.pagination.page * this.pagination.pagesize >= this.pagination.total) {
+                this.pagination.noMore = true
+            } else {
+                this.pagination.noMore = false
+            }
+        }
+
+        return learnList
+    }
+
+    dealThisMeta() {
+        let head = {
+            title: "",
+            keyword: "",
+            description: "",
+            h1: "",
+            canonical: "",
+            metaLocation: ""
+        }
+
+        if (this.req) {
+            const { headers: { host }, url } = this.req
+
+            //拼接canonical
+            if (host.indexOf('local') !== -1) {
+              head.canonical = 'http://' + host + url
+            } else {
+              head.canonical = 'https://' + host + url
+            }
+        }
+
+        if (this.cateNameTwo) {
+            // 分类页
+            head.title = `${this.cateNameTwo}技能服务-程序员客栈技术服务`;
+            head.keyword = `${this.cateNameTwo}开发,${this.cateNameTwo}编程,自学${this.cateNameTwo},${this.cateNameTwo}教学`;
+            head.description = "技能服务是程序员客栈远程工作平台为企业和自由职业者提供的标准化数字服务,通过标准定价的模块化技能,帮助企业和自由职业者快速达成合作。";
+        } else {
+            // 列表页,无筛选参数
+            head.title = "程序员客栈技能商城-【程序员客栈技能服务】";
+            head.keyword = "程序员客栈技能商城,远程工作,Logo设计,网页设计,微信公众号运营,PPT设计,社群运营,文案编辑,视频剪辑,音频录制,翻译";
+            head.description = "技能服务是程序员客栈远程工作平台为企业和自由职业者提供的标准化数字服务,通过标准定价的模块化技能,帮助企业和自由职业者快速达成合作。";
+        }
+
+        return head
+    }
+}

+ 1 - 1
components/skill/dealSeoDetail.js

@@ -21,7 +21,7 @@ export default class DealSeoData {
 
         // 重定向
         if (path.indexOf('/frontend/skill/detail') > -1) {
-            this.redirect(301, '/l/' + sale_id)
+            this.redirect(301, '/s/' + sale_id)
         }
 
         const skillDetail = await this._getSkillDetail(sale_id)

+ 1 - 1
components/skill/dealSeoList.js

@@ -41,7 +41,7 @@ export default class DealSeoData {
 
         // 重定向
         if (path.indexOf('/frontend/skill/list') > -1) {
-            this.redirect(301, '/learn/' + lastMatch)
+            this.redirect(301, '/skill/' + lastMatch)
         }
 
         let skillCate = await this._getSkillCate()

+ 102 - 0
pages/frontend/learn/detail/_id.vue

@@ -0,0 +1,102 @@
+<template>
+    <ErrorPage404 v-if="!isExist"></ErrorPage404>
+    <div v-else :class="mobile ? 'mobileMain' : 'mobileWeb'" :style="{marginTop: mainMarginTop}">
+        <div class="learn-detail-wrapper" v-if="!mobile">
+            web 端详情页
+        </div>
+        <div class="learn-detail-wrapper-mobile" v-else>
+            移动端详情
+        </div>
+    </div>
+</template>
+
+<script>
+import {mapState} from "vuex"
+import DealSeoDetail from "@/components/learn/dealSeoDetail"
+import qs from "qs"
+import ErrorPage404 from "@/components/error_page/404.vue"
+
+export default {
+    name: 'SeoLearnDetail',
+    data () {
+        return {
+            baseUrl: '',
+            isWeixinApp: true,
+        }
+    },
+    components: {
+        ErrorPage404
+    },
+    head() {
+        const {
+            title = "",
+            keyword = "",
+            description = "",
+            h1 = "",
+            canonical = "",
+            metaLocation
+        } = this.head || {}
+        let obj = {
+            title: title,
+            meta: [{
+                name: "keywords",
+                content: keyword
+            }, {
+                name: "description",
+                content: description
+            }, {
+                name: "h1",
+                content: h1
+            }, {
+                name: "viewport",
+                content: "width=device-width, initial-scale=1.0, viewport-fit=cover"
+            }],
+            link: [{rel: "canonical", href: canonical}]
+        }
+        if (metaLocation) {
+            obj.meta.push({name: "location", content: metaLocation})
+        }
+        return obj
+    },
+    computed: {
+        ...mapState(["deviceType"]),
+        showWxHeader () {
+            return !this.deviceType.app && !this.isWeixinApp &&
+                (this.deviceType.android || this.deviceType.ios)
+        },
+        mainMarginTop () {
+            if (this.mobile && this.showWxHeader) {
+                return '0 !important'
+            } else if (this.mobile) {
+                return '0px !important'
+            } else {
+                return '20px !important'
+            }
+        }
+    },
+    async asyncData ({...params}) {
+        let dealDataObj = new DealSeoDetail(params)
+        let ans = await dealDataObj.dealData()
+
+        return {
+            ...ans
+        }
+    },
+    mounted () {
+        const self = this
+        this.baseUrl = this.$store.state.domainConfig.siteUrl
+        this.isWeixinApp = navigator.userAgent.indexOf("miniProgram") > -1
+    },
+    methods: {
+    }
+}
+</script>
+
+<style lang="scss" scoped>
+@import "@/assets/css/learn/detail/_id.scss";
+</style>
+<style lang="scss">
+.wx-header-custom-detail {
+    position: relative !important;
+}
+</style>

+ 410 - 0
pages/frontend/learn/list.vue

@@ -0,0 +1,410 @@
+<template>
+    <div
+        :class="mobile ? 'mobileMain' : ''"
+        :style="{marginTop: mainMarginTop, marginBottom: mobile ? '0px' : '30px !important'}">
+        <div class="learn-wrapper" v-if="!mobile">
+            <div class="learn-top">
+                <div class="tabs">
+                    <a href="/skill/" class="tabs-item">技能服务</a>
+                    <a href="/consult/" class="tabs-item">咨询服务</a>
+                    <div class="tabs-item active">客栈学院</div>
+                    <!-- <el-button class="add-btn" @click="handleClickAdd">发布课程</el-button> -->
+                </div>
+                <div class="learn-category-wrapper">
+                    <!-- <div class="category-title">技术分类</div> -->
+                    <!-- 一级分类内容 -->
+                    <div
+                        class="category-one-wrapper"
+                        :class="categoryExpanded ? 'expand' : ''">
+                        <!-- 更多 -->
+                        <!-- <div
+                            class="category-more"
+                            @click="handleClickExpandCategory">{{ categoryExpanded ? '收起' : '更多' }}</div> -->
+                        <!-- 全部 -->
+                        <a
+                            href="/learn/"
+                            class="category-one-item"
+                            :class="pagination.selectedCateIdOne == '' ? 'active' : ''">全部</a>
+                        <!-- 一级分类 -->
+                        <a
+                            v-for="categoryOne in learnCate"
+                            :key="categoryOne.value"
+                            class="category-one-item"
+                            :class="pagination.selectedCateIdOne == categoryOne.value ? 'active' : ''"
+                            :href="`/learn/${categoryOne.value}/`">{{ categoryOne.label }}</a>
+                    </div>
+                    <!-- 二级分类内容 -->
+                    <div class="category-two-wrapper">
+                        <div
+                            class="category-two-content"
+                            v-for="categoryOne in learnCate"
+                            :key="`cate-two-parnet${categoryOne.value}`"
+                            v-show="pagination.selectedCateIdOne == categoryOne.value">
+                            <a
+                                class="category-two-item"
+                                :class="pagination.selectedCateIdTwo == categoryTwo.value ? 'active' : ''"
+                                :href="`/learn/${categoryTwo.value}/`"
+                                v-for="categoryTwo in categoryOne.children"
+                                :key="`cate-two-${categoryTwo.value}`">{{ categoryTwo.label }}</a>
+                        </div>
+                    </div>
+                </div>
+            </div>
+            <div class="learn-content">
+                <div class="learn-list-wrapper" v-if="learnList.length">
+                    <div
+                        class="learn-item"
+                        v-for="item in learnList"
+                        :key="item.sale_id">
+                        <a :href="`/s/${item.sale_id}`">
+                            <img class="cover" :src="item.coverImage" alt="learnCover,cover">
+                        </a>
+                        <a class="owner-wrapper" :href="`/wo/${item.user.uid}/learn`">
+                            <img class="avatar" :src="item.user.icon_url" alt="avatar">
+                            <div class="nickname">{{ item.user.nickname }}</div>
+                        </a>
+                        <a class="title" :href="`/s/${item.sale_id}`">{{ item.title }}</a>
+                        <div class="price-wrapper">
+                            <div class="price-text">¥{{ item.price }}</div>
+                            <div  v-if="item.buy_num>0" class="buy-num">{{ item.buy_num }}人已学习</div>
+                        </div>
+                    </div>
+                </div>
+                <div class="result-empty-wrapper" v-else>
+                    <img src="@/assets/img/common/empty@2x.png" alt="empty">
+                    <span>暂无搜索内容</span>
+                </div>
+                <div class="pagination-wrapper" v-if="pagination.total > pagination.pagesize">
+                    <el-pagination
+                        background
+                        layout="prev, pager, next"
+                        :current-page="pagination.page"
+                        :total="pagination.total"
+                        :page-size="pagination.pagesize"
+                        @current-change="handlePageChange">
+                    </el-pagination>
+                </div>
+            </div>
+        </div>
+    </div>
+</template>
+
+<script>
+import {mapState} from "vuex"
+import DealSeoList from "@/components/learn/dealSeoList"
+import qs from "qs"
+
+export default {
+    name: 'SeoLearnList',
+    data () {
+        return {
+            baseUrl: '',
+            // firstLoad: true,
+            isWeixinApp: true,
+            categoryExpanded: true, // 更多按钮不要,默认为展开状态
+            showCategoryDrawer: false,
+            currentDrawerCategoryId: 0,
+            currentDrawerCategoryIndex: 0
+        }
+    },
+    head() {
+        const {
+            title = "",
+            keyword = "",
+            description = "",
+            h1 = "",
+            canonical = "",
+            metaLocation
+        } = this.head || {}
+        let obj = {
+            title: title,
+            meta: [{
+                name: "keywords",
+                content: keyword
+            }, {
+                name: "description",
+                content: description
+            }, {
+                name: "h1",
+                content: h1
+            }],
+            link: [{rel: "canonical", href: canonical}]
+        }
+        if (metaLocation) {
+            obj.meta.push({name: "location", content: metaLocation})
+        }
+        return obj
+    },
+    computed: {
+        ...mapState(["deviceType"]),
+        showWxHeader () {
+            return !this.deviceType.app && !this.isWeixinApp &&
+                (this.deviceType.android || this.deviceType.ios)
+        },
+        mainMarginTop () {
+            if (this.mobile && this.showWxHeader) {
+                return '64px !important'
+            } else if (this.mobile) {
+                return '0px !important'
+            } else {
+                return '20px !important'
+            }
+        }
+    },
+    async asyncData ({...params}) {
+        let dealDataObj = new DealSeoList(params)
+        let ans = await dealDataObj.dealData()
+
+        return {
+            ...ans
+        }
+    },
+    mounted () {
+        this.baseUrl = this.$store.state.domainConfig.siteUrl
+        this.isWeixinApp = navigator.userAgent.indexOf("miniProgram") > -1
+    },
+    methods: {
+        /** 分页获取技能列表数据 */
+        _getLearnList () {
+            const self = this
+            const data = {
+                type: 1,
+                page: this.pagination.page,
+                page_size: this.pagination.pagesize,
+                cate_id: this.pagination.selectedCateIdTwo,
+                status: 2,
+                owner_type: 1,
+                root_type: this.root_type
+            }
+
+            this.pagination.loading = true
+            this.pagination.noMore = false
+
+            this.$axios.$post('/api/sale/saleList', data).then(res => {
+                if (Number(res.status) === 1) {
+                    let learnList = res.data.list || []
+                    learnList.forEach((item) => {
+                        let imageList = item.image.split(',')
+                        item.coverImage = imageList[0] || ''
+                        imageList.splice(0, 1)
+                        item.imageList = imageList
+                    })
+
+                    if (self.mobile) {
+                        self.learnList = self.learnList.concat(learnList)
+                    } else {
+                        self.learnList = learnList
+                    }
+
+                    self.pagination.total = res.data.total
+                    self.pagination.pagesize = res.data.page_size || 9
+                    if (self.pagination.page * self.pagination.pagesize >= self.pagination.total) {
+                        console.log('noMore true', self.pagination)
+                        self.pagination.noMore = true
+                    } else {
+                        console.log('noMore false', self.pagination)
+                        self.pagination.noMore = false
+                    }
+                }
+            }).then(() => {
+                self.pagination.loading = false
+            })
+        },
+        /** 点击展开、收起 */
+        handleClickExpandCategory () {
+            this.categoryExpanded = !this.categoryExpanded
+        },
+        /** 点击一级分类时 */
+        handleClickCategoryOne (id) {
+            if (id === 0) {
+                // 点击全部时,移除筛选分类
+                this.pagination.selectedCateIdOne = id
+                this.pagination.selectedCateIdTwo = ''
+                this.currentDrawerCategoryId = ''
+                this.pagination.page = 1
+                this.learnList = []
+                window.scroll(0, 0)
+
+                this._getLearnList()
+                return
+            }
+            if (this.pagination.selectedCateIdOne !== id) {
+                this.pagination.selectedCateIdOne = id
+            }
+        },
+        /** 点击二级分类时:移动端 */
+        handleClickCategoryTwo (id) {
+            if (this.pagination.selectedCateIdTwo === id) {
+                this.pagination.selectedCateIdTwo = ''
+                this.currentDrawerCategoryId = ''
+            } else {
+                this.pagination.selectedCateIdTwo = id
+                this.currentDrawerCategoryId = id
+            }
+            this.pagination.page = 1
+            this.learnList = []
+            window.scroll(0, 0)
+
+            this._getLearnList()
+        },
+        /** 分页页码改变时 */
+        handlePageChange (val) {
+            let query = {
+                page: val
+            }
+            if (this.root_type && Number(this.root_type) > 0) {
+                query.root_type = this.root_type
+            }
+            window.location.href = `${window.location.origin}${window.location.pathname}?${qs.stringify(query)}`
+        },
+        /** mobile 加载更多 */
+        handleLoadMoreSkill () {
+            if (this.pagination.loading) {
+                return
+            }
+
+            this.pagination.page++
+            this._getLearnList()
+        },
+        /** 点击筛选时 */
+        handleShowCategoryDrawer () {
+            this.showCategoryDrawer = true
+        },
+        /**
+         * 点击 mobile 分类 drawer 一级分类
+         */
+        handleClickDrawerCategoryOne (id) {
+            if (id === 0) {
+                this.showCategoryDrawer = false
+                return
+            }
+            if (id !== this.currentDrawerCategoryIndex) {
+                this.currentDrawerCategoryIndex = id
+            }
+        },
+        /**
+         * 点击 mobile 分类 drawer 二级分类
+         */
+        handleClickDrawerCategoryTwo (id) {
+            if (this.currentDrawerCategoryId === id) {
+                this.pagination.selectedCateIdTwo = ''
+            } else {
+                this.pagination.selectedCateIdTwo = id
+            }
+            this.currentDrawerCategoryId = id
+            this.showCategoryDrawer = false
+            this.pagination.page = 1
+            this.learnList = []
+            window.scroll(0, 0)
+
+            this._getLearnList()
+        },
+        /**
+         * 点击 mobile 的一项技能时
+         */
+        handleClickSkillItem (saleId) {
+            if (this.deviceType.android || this.deviceType.ios) {
+                // 端跳转
+                let jumpUrl = `${this.baseUrl}/s/${saleId}`
+                location.href = `proginn://webview?url=${jumpUrl}`
+            } else {
+                // web 跳转
+                location.href = `/s/${saleId}`
+            }
+        },
+        /**
+         * 点击成为讲师
+         */
+        handleClickAdd () {
+            location.href = '/workbench/skill/index'
+        }
+    }
+}
+</script>
+
+<style lang="scss" scoped>
+@import "@/assets/css/learn/list.scss";
+</style>
+<style lang="scss">
+.category-drawer {
+    .el-drawer {
+        height: 100vh !important;
+        .el-drawer__body {
+            position: relative;
+            width: 100%;
+            display: flex;
+        }
+    }
+    .drawer-category-one {
+        width: 100px;
+        height: 100vh;
+        padding-bottom: 34px;
+        background: #f4f5f9;
+        overflow-x: hidden;
+        overflow-y: auto;
+        -webkit-overflow-scrolling: touch;
+        &::-webkit-scrollbar {
+            display: none;
+        }
+        .drawer-category-one-item {
+            width: 100%;
+            height: 50px;
+            line-height: 50px;
+            text-align: center;
+            font-size: 15px;
+            font-family: PingFangSC, PingFangSC-Medium;
+            font-weight: 500;
+            color: #222222;
+            background: inherit;
+            &.active {
+                color: #308eff;
+                background: #ffffff;
+            }
+        }
+    }
+    .drawer-category-two {
+        width: calc(100% - 100px);
+        height: 100vh;
+        padding: 4px 10px 34px;
+        background: #ffffff;
+        overflow-x: hidden;
+        overflow-y: auto;
+        -webkit-overflow-scrolling: touch;
+        &::-webkit-scrollbar {
+            display: none;
+        }
+        .drawer-category-two-wrapper {
+            width: 100%;
+            display: flex;
+            flex-wrap: wrap;
+            .drawer-category-two-item {
+                margin: 8px 8px 0 0;
+                padding: 0 12px;
+                height: 35px;
+                line-height: 35px;
+                background: rgba(244,245,249,.8);
+                border-radius: 4px;
+                // opacity: 0.8;
+                font-size: 13px;
+                font-family: PingFangSC, PingFangSC-Regular;
+                font-weight: 400;
+                color: #222222;
+                &.active {
+                    height: 33px;
+                    line-height: 33px;
+                    border: 1px solid #308eff;
+                    background: #ffffff;
+                    font-size: 12px;
+                    font-family: PingFangSC, PingFangSC-Medium;
+                    font-weight: 500;
+                    color: #308eff;
+                }
+            }
+        }
+    }
+}
+.wx-header-custom-list {
+    position: fixed !important;
+    z-index: 11 !important;
+}
+</style>

+ 8 - 4
plugins/seoRouter.js

@@ -58,7 +58,7 @@ const extendRoutes = (routes, resolve) => {
       component: resolve(__dirname, '../pages/work_down/index.vue')
     }],
 
-    // 服务:技能、咨询
+    // 服务:技能、咨询、客栈学院
     ...[{
       name: 'SeoSkillList_0',
       path: '/frontend/skill/list/*',
@@ -68,17 +68,21 @@ const extendRoutes = (routes, resolve) => {
       path: '/frontend/consult/list/*',
       component: resolve(__dirname, '../pages/frontend/consult/list.vue')
     },{
+      name: 'SeoLearnList_0',
+      path: '/frontend/learn/list/*',
+      component: resolve(__dirname, '../pages/frontend/learn/list.vue')
+    },{
       name: 'SeoLearnList',
       path: '/learn',
-      component: resolve(__dirname, '../pages/frontend/skill/list.vue')
+      component: resolve(__dirname, '../pages/frontend/learn/list.vue')
     },{
       name: 'SeoLearnList_1',
       path: '/learn/*',
-      component: resolve(__dirname, '../pages/frontend/skill/list.vue')
+      component: resolve(__dirname, '../pages/frontend/learn/list.vue')
     },{
       name: 'SeoLearnDetail',
       path: '/l/:id?',
-      component: resolve(__dirname, '../pages/frontend/skill/detail/_id.vue')
+      component: resolve(__dirname, '../pages/frontend/learn/detail/_id.vue')
     },{
       name: 'SeoConsultList',
       path: '/consult',