younger_times
2023-07-12 5a590aaf3fd9ed5f9cfd2d54e72c904ce4918e7f
完善“课程详情”
14个文件已修改
1个文件已添加
606 ■■■■ 已修改文件
WanPai.xcodeproj/project.pbxproj 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WanPai/Config/Enums.swift 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WanPai/Model/CommonModels.swift 23 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WanPai/Network/NetworkRequest.swift 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WanPai/Network/Services.swift 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WanPai/Root/Course/VC/CourseDetailApplyVC.swift 151 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WanPai/Root/Course/VC/CourseDetailApplyVC.xib 28 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WanPai/Root/Course/VC/CourseDetailVC.swift 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WanPai/Root/Course/VC/StudentCourseDetailVC.swift 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
WanPai/Root/Home/VC/HomeVC.swift 72 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WanPai/Root/Other/CCell/Common_1_CCell.swift 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WanPai/Root/Other/TCell/StudentInfoTCell.swift 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WanPai/Root/Other/TCell/StudentInfoTCell.xib 9 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WanPai/Root/Welfare/VC/WelfareWeeklyDetailVC.swift 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
WanPai/ViewModel/RefreshModel.swift 188 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WanPai.xcodeproj/project.pbxproj
@@ -47,6 +47,7 @@
        133A618D2A4E7AF90066C4E6 /* CustomerContentTCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 133A618B2A4E7AF90066C4E6 /* CustomerContentTCell.xib */; };
        13489E0A2A4C41A400155744 /* ProfileVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13489E082A4C41A400155744 /* ProfileVC.swift */; };
        13489E0B2A4C41A400155744 /* ProfileVC.xib in Resources */ = {isa = PBXBuildFile; fileRef = 13489E092A4C41A400155744 /* ProfileVC.xib */; };
        134A750A2A5D0D64006D14AE /* RefreshModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 134A75092A5D0D64006D14AE /* RefreshModel.swift */; };
        1353D5752A56CA0A00539FCA /* Services.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1353D5732A56CA0A00539FCA /* Services.swift */; };
        1353D5762A56CA0A00539FCA /* NetworkRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1353D5742A56CA0A00539FCA /* NetworkRequest.swift */; };
        1355ABFA2A4BE9FF002B25E4 /* WelfareCouponsSubListVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1355ABF92A4BE9FF002B25E4 /* WelfareCouponsSubListVC.swift */; };
@@ -292,6 +293,7 @@
        133A618B2A4E7AF90066C4E6 /* CustomerContentTCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = CustomerContentTCell.xib; sourceTree = "<group>"; };
        13489E082A4C41A400155744 /* ProfileVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileVC.swift; sourceTree = "<group>"; };
        13489E092A4C41A400155744 /* ProfileVC.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ProfileVC.xib; sourceTree = "<group>"; };
        134A75092A5D0D64006D14AE /* RefreshModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshModel.swift; sourceTree = "<group>"; };
        1353D5732A56CA0A00539FCA /* Services.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Services.swift; sourceTree = "<group>"; };
        1353D5742A56CA0A00539FCA /* NetworkRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkRequest.swift; sourceTree = "<group>"; };
        1355ABF92A4BE9FF002B25E4 /* WelfareCouponsSubListVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelfareCouponsSubListVC.swift; sourceTree = "<group>"; };
@@ -586,6 +588,7 @@
            isa = PBXGroup;
            children = (
                1375463F2A57F545001FA77A /* UserModel.swift */,
                134A75092A5D0D64006D14AE /* RefreshModel.swift */,
            );
            path = ViewModel;
            sourceTree = "<group>";
@@ -1523,6 +1526,7 @@
                139A39FA2A413C6500737AFB /* WelfareWeeklyTCell.swift in Sources */,
                8DA51DF72A32BE270085F4BA /* Enums.swift in Sources */,
                130E402A2A4EC33C003A3D75 /* SearchStoreDetailHeadView.swift in Sources */,
                134A750A2A5D0D64006D14AE /* RefreshModel.swift in Sources */,
                1304B8582A4EAEBA000D4F2E /* CommonBannerView.swift in Sources */,
                8D86DE852A3307A400A754EF /* CourseSubTypeView.swift in Sources */,
                8D79A5932A395BF40029874B /* ActivityStudentListVC.swift in Sources */,
WanPai/Config/Enums.swift
@@ -1,19 +1,19 @@
//
//  Enums.swift
//  WanPai
//
//  Created by 杨锴 on 2023/6/9.
//
    //
    //  Enums.swift
    //  WanPai
    //
    //  Created by 杨锴 on 2023/6/9.
    //
import Foundation
import HandyJSON
/// 登录类型
    /// 登录类型
enum LoginType{
    case pwd,smsCode
}
/// 支付方式
    /// 支付方式
enum PayType:Int{
    case aliPay = 0
    case wechat = 1
@@ -26,7 +26,7 @@
    case activity
}
/// 获取短信验证码类型
    /// 获取短信验证码类型
enum GetSMSCodeType:Int{
    case login = 1
    case register = 2
@@ -73,7 +73,7 @@
enum HomeItemType:Int,HandyJSONEnum{
    //1=报名玩湃课程,2=预约场地,3=报名赛事及活动,4=免费福利,5=线上课程积分,6=购买优惠门票,7=看视频得奖励,8=智慧球场)
        //1=报名玩湃课程,2=预约场地,3=报名赛事及活动,4=免费福利,5=线上课程积分,6=购买优惠门票,7=看视频得奖励,8=智慧球场)
    case none = 0
    case course = 1
    case booking = 2
@@ -146,3 +146,13 @@
    case cash = 1 //现金
    case coin = 2 //玩湃币
}
enum CouponType:Int,HandyJSONEnum{
    case none = 0
        ///满减券
    case discount = 1
        /// 代金券
    case voucher = 2
        /// 体验券
    case experience = 3
}
WanPai/Model/CommonModels.swift
@@ -43,15 +43,28 @@
struct CourseDetailListModel:HandyJSON {
    var classHours: Int = 0
    var id: Int = 0
    var originalPrice: Double = 0
    var paymentPrice: Double = 0
    var payType: Int = 0
    var playPaiCoin: Int = 0
    var vipPrice: Double = 0
    var originalPrice:Double?
    var paymentPrice:Double = 0
    var payType: PaymentType = .cash
    var playPaiCoin:Int?
    var vipPrice:Double?
}
struct CourseDetailStudentModel:HandyJSON {
    var age: Int = 0
    var id: Int = 0
    var name = ""
    var phone: String = ""
}
struct CouponInfoModel:HandyJSON{
    ///有效时间
    var effectiveTime: String = ""
        /// 优惠内容
    var favorable: String = ""
    var id: Int = 0
    var name: String = ""
    var type:CouponType = .none
        /// 使用条件
    var useCondition: String = ""
}
WanPai/Network/NetworkRequest.swift
@@ -137,6 +137,10 @@
        let content = paramsArray.joined(separator: "&")
        params += ["sign": "\(content.jq_hmacBase64(algorithm: .SHA1, key: SHAKEY))"]
        #if DEBUG
        LogInfo("签名:\(content) ----- \(content.jq_hmacBase64(algorithm: .SHA1, key: SHAKEY))")
        #endif
        return self.params
    }
    
WanPai/Network/Services.swift
@@ -23,8 +23,8 @@
    class func homeInfo()->Observable<BaseResponse<HomeStoreModel>>{
        let params = ParamsAppender.build(url: All_Url)
            .interface(url: "/account/api/appUser/queryJoinPlayPai")
            .append(key: "lat", value: locationTool.currentLocation?.coordinate.latitude)
            .append(key: "lon", value: locationTool.currentLocation?.coordinate.longitude)
            .append(key: "lat", value: locationTool.currentLocation?.coordinate.latitude.string)
            .append(key: "lon", value: locationTool.currentLocation?.coordinate.longitude.string)
        return NetworkRequest.request(params: params, method: .post, progress: false)
    }
@@ -94,8 +94,8 @@
    class func queryCourseList(typeId:Int? = nil,distanceSort:SortType? = nil,salesRanking:SortType? = nil,search:String? = nil,storeId:Int? = nil)->Observable<BaseResponse<[CourseItemModel]>>{
        let params = ParamsAppender.build(url: All_Url)
            .interface(url: "/course/api/course/queryCourseList")
            .append(key: "lat", value: locationTool.currentLocation?.coordinate.latitude)
            .append(key: "lon", value: locationTool.currentLocation?.coordinate.longitude)
            .append(key: "lat", value: locationTool.currentLocation?.coordinate.latitude.string)
            .append(key: "lon", value: locationTool.currentLocation?.coordinate.longitude.string)
            .append(key: "coursePackageTypeId", value: typeId)
            .append(key: "distanceSort", value: distanceSort?.rawValue)
            .append(key: "salesRanking", value: salesRanking?.rawValue)
@@ -113,8 +113,8 @@
    class func queryStoreList()->Observable<BaseResponse<[NormalSimpleModel]>>{
        let params = ParamsAppender.build(url: All_Url)
            .interface(url: "/other/base/store/queryStoreLists")
            .append(key: "lat", value: locationTool.currentLocation?.coordinate.latitude)
            .append(key: "lon", value: locationTool.currentLocation?.coordinate.longitude)
            .append(key: "lat", value: locationTool.currentLocation?.coordinate.latitude.string)
            .append(key: "lon", value: locationTool.currentLocation?.coordinate.longitude.string)
        return NetworkRequest.request(params: params, method: .post, progress: false)
    }
@@ -122,10 +122,20 @@
        let params = ParamsAppender.build(url: All_Url)
            .interface(url: "/course/api/course/queryCourseInfo")
            .append(key: "id", value: id)
            .append(key: "lat", value: locationTool.currentLocation?.coordinate.latitude)
            .append(key: "lon", value: locationTool.currentLocation?.coordinate.longitude)
            .append(key: "lat", value: locationTool.currentLocation?.coordinate.latitude.string)
            .append(key: "lon", value: locationTool.currentLocation?.coordinate.longitude.string)
        return NetworkRequest.request(params: params, method: .post, progress: true)
    }
    class func queryAvaiableCopons(id:Int,price:Double)->Observable<BaseResponse<[CouponInfoModel]>>{
        let params = ParamsAppender.build(url: All_Url)
            .interface(url: "/activity/api/coupon/queryAvailableCouponList")
            .append(key: "coursePackageId", value: id)
            .append(key: "lat", value: locationTool.currentLocation?.coordinate.latitude.string)
            .append(key: "lon", value: locationTool.currentLocation?.coordinate.longitude.string)
            .append(key: "price", value: "\(price)")
        return NetworkRequest.request(params: params, method: .post, progress: false)
    }
}
    // MARK: -- 其他
WanPai/Root/Course/VC/CourseDetailApplyVC.swift
@@ -1,9 +1,9 @@
//
//  CourseDetailApplyVC.swift
//  WanPai
//
//  Created by 杨锴 on 2023/6/9.
//
    //
    //  CourseDetailApplyVC.swift
    //  WanPai
    //
    //  Created by 杨锴 on 2023/6/9.
    //
import UIKit
import JQTools
@@ -15,20 +15,58 @@
    @IBOutlet weak var cons_collectHei: NSLayoutConstraint!
    @IBOutlet weak var btn_addStudent: QMUIButton!
    @IBOutlet weak var tableView: UITableView!
    @IBOutlet weak var cons_collHei: NSLayoutConstraint!
    @IBOutlet weak var cons_tableHei: NSLayoutConstraint!
    @IBOutlet weak var btn_coupon: TapBtn!
    @IBOutlet weak var img_cover: UIImageView!
    @IBOutlet weak var label_title: UILabel!
    @IBOutlet weak var label_listenWeek: UILabel!
    @IBOutlet weak var label_listenTime: UILabel!
    @IBOutlet weak var label_store: UILabel!
    @IBOutlet weak var label_address: UILabel!
    @IBOutlet weak var label_price: UILabel!
    @IBOutlet weak var label_originPrice: UILabel!
    @IBOutlet weak var label_vipPrice: UILabel!
    @IBOutlet weak var label_coin: UILabel!
    @IBOutlet weak var btn_hasCoupon: TapBtn!
    @IBOutlet weak var studentTableView: UITableView!
    var CellW:Double!
    var CellH:Double!
    private var detailModel:CourseDetailModel?
    private var selectClassIndex:Int = 0
    private var CellW:Double!
    private var CellH:Double!
    private var studentModels = [CourseDetailStudentModel]()
    private var couponModels = [CouponInfoModel]()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        title = "课程详情"
        if let m = detailModel{
            img_cover.sd_setImage(with: URL(string: m.coverDrawing))
            label_title.text = m.name
            label_listenWeek.text = m.weeks.joined(separator: "、")
            label_listenTime.text = m.times
            label_store.text = m.storeName
            label_address.text = m.storeAddress
            changePrice(selectClassIndex)
            if let stu = m.student{studentModels.append(stu)}
            cons_collHei.constant = ceil(Double(m.list.count) / 3.0) * CellH + floor(Double(m.list.count) / 3.0) * 21.0
            collectionView.reloadData()
            cons_tableHei.constant = CGFloat(studentModels.count * 87)
            tableView.reloadData()
        }
    }
    
    override func setUI() {
        btn_hasCoupon.isHidden = true
        CellW = (JQ_ScreenW - 155) / 3.0
        CellH = CellW * 0.439
        
@@ -44,8 +82,22 @@
        btn_addStudent.imagePosition = .right
        btn_addStudent.spacingBetweenImageAndTitle = 3
    }
    init(detailModel:CourseDetailModel) {
        super.init(nibName: nil, bundle: nil)
        self.detailModel = detailModel
    }
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    @IBAction func couponAction(_ sender: TapBtn) {
        guard couponModels.count != 0 else {
            alertError(msg: "暂无优惠券");return
        }
        CouponChooseView.show()
    }
    
@@ -56,7 +108,6 @@
            let vc = AddStudentVC()
            self?.push(vc: vc)
        }
    }
    
    @IBAction func paymentAction(_ sender: UIButton) {
@@ -67,21 +118,85 @@
            self.present(vc, animated: true)
        }
    }
    private func changePrice(_ index:Int){
        if let subM = detailModel?.list[index]{
            label_price.text = subM.paymentPrice.currency()
            label_originPrice.isHidden = subM.originalPrice == nil
            label_coin.isHidden = subM.playPaiCoin == nil
            label_vipPrice.isHidden = subM.vipPrice == nil
                //原价
            if let originPrice =  subM.originalPrice{
                let attribute = AttributedStringbuilder.build().add(string: originPrice.currency(), withFont: UIFont.systemFont(ofSize: 16, weight: .semibold), withColor: UIColor(hexStr: "#3F3F3F").withAlphaComponent(0.58)).underLine(color: UIColor(hexStr: "#3F3F3F").withAlphaComponent(0.58))
                label_originPrice.attributedText = attribute.mutableAttributedString
            }
                //玩湃币
            if let paiCoin = subM.playPaiCoin{
                let coinAttribute = AttributedStringbuilder.build()
                    .add(string: "玩湃币:", withFont: UIFont.systemFont(ofSize: 14, weight: .semibold), withColor: UIColor(hexStr: "#3F3F3F"))
                    .add(string: "\(paiCoin)币", withFont: UIFont.systemFont(ofSize: 14, weight: .semibold), withColor: UIColor(hexStr: "#F21313"))
                label_coin.attributedText = coinAttribute.mutableAttributedString
            }
                //会员价
            if let vipPrice = subM.vipPrice{
                let vipAttribute = AttributedStringbuilder.build()
                    .add(string: "会员价:", withFont: UIFont.systemFont(ofSize: 14, weight: .semibold), withColor: UIColor(hexStr: "#3F3F3F"))
                    .add(string: vipPrice.currency(), withFont: UIFont.systemFont(ofSize: 14, weight: .semibold), withColor: UIColor(hexStr: "#F21313"))
                label_vipPrice.attributedText = vipAttribute.mutableAttributedString
            }
        }
        queryCouponInfo()
    }
    private func queryCouponInfo(){
        if let subM = detailModel?.list[selectClassIndex]{
            var price:Double?
            switch subM.payType{
                case .cash:
                    price = subM.originalPrice == nil ? subM.vipPrice : subM.originalPrice
                case .coin:
                    if let coin = subM.playPaiCoin{price = Double(coin)}
            }
            guard price != nil else {
                LogError("会员优惠价格出现问题:nil");return
            }
            Services.queryAvaiableCopons(id: subM.id, price: price!).subscribe(onNext: { [weak self] data in
                self?.btn_hasCoupon.isHidden = (data.data?.count ?? 0) == 0
                self?.couponModels = data.data ?? []
            }).disposed(by: disposeBag)
        }
    }
}
extension CourseDetailApplyVC:UICollectionViewDelegate{
    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        selectClassIndex = indexPath.row
        collectionView.reloadData()
        changePrice(selectClassIndex)
    }
}
extension CourseDetailApplyVC:UICollectionViewDataSource{
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
       let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "_Common_1_CCell", for: indexPath) as! Common_1_CCell
        let m = detailModel!.list[indexPath.row]
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "_Common_1_CCell", for: indexPath) as! Common_1_CCell
        cell.isSelected = indexPath.row == selectClassIndex
        cell.courseDetailListModel = m
        return cell
    }
    
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return 3
        return detailModel?.list.count ?? 0
    }
}
@@ -102,12 +217,12 @@
extension CourseDetailApplyVC:UITableViewDataSource{
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return 1
        return studentModels.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "_StudentInfoTCell") as! StudentInfoTCell
        cell.studentModel = studentModels[indexPath.row]
        return cell
    }
    
WanPai/Root/Course/VC/CourseDetailApplyVC.xib
@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="21507" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="21701" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
    <device id="retina6_12" orientation="portrait" appearance="light"/>
    <dependencies>
        <deployment identifier="iOS"/>
        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21505"/>
        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21679"/>
        <capability name="Named colors" minToolsVersion="9.0"/>
        <capability name="Safe area layout guides" minToolsVersion="9.0"/>
        <capability name="System colors in document resources" minToolsVersion="11.0"/>
@@ -14,9 +14,21 @@
            <connections>
                <outlet property="btn_addStudent" destination="kv0-E8-XGD" id="H56-eE-BCc"/>
                <outlet property="btn_coupon" destination="XNI-Vj-heV" id="es4-Cw-MCW"/>
                <outlet property="btn_hasCoupon" destination="XNI-Vj-heV" id="680-zV-BEX"/>
                <outlet property="collectionView" destination="ZtR-nH-Ly1" id="Snf-0R-x4b"/>
                <outlet property="cons_collHei" destination="wOm-8j-9fG" id="gZo-vk-a95"/>
                <outlet property="cons_collectHei" destination="wOm-8j-9fG" id="ycB-BH-9OQ"/>
                <outlet property="cons_tableHei" destination="YrT-fE-DXP" id="wAi-wu-Afa"/>
                <outlet property="img_cover" destination="SMj-2r-hml" id="eCI-j3-X6o"/>
                <outlet property="label_address" destination="dq4-Ry-HOa" id="TTi-yc-w1v"/>
                <outlet property="label_coin" destination="AuS-Gc-geQ" id="rBY-uI-jPU"/>
                <outlet property="label_listenTime" destination="gAj-GU-kf7" id="F9v-Rl-tLX"/>
                <outlet property="label_listenWeek" destination="z0j-zm-BuW" id="6Sj-GF-NyT"/>
                <outlet property="label_originPrice" destination="hAD-m3-5Ns" id="mBy-fz-n85"/>
                <outlet property="label_price" destination="O3i-VB-ddE" id="4xG-PL-jeC"/>
                <outlet property="label_store" destination="OPI-Cq-tpZ" id="Yo2-ua-F9q"/>
                <outlet property="label_title" destination="FDE-aj-7d9" id="var-Cz-Fnt"/>
                <outlet property="label_vipPrice" destination="55J-SL-KdW" id="xZ1-NL-dkq"/>
                <outlet property="studentTableView" destination="CMD-gX-7vo" id="Xu0-eU-5SD"/>
                <outlet property="tableView" destination="CMD-gX-7vo" id="dC7-Q4-Es3"/>
                <outlet property="view" destination="i5M-Pr-FkT" id="sfx-zR-JGt"/>
@@ -381,7 +393,7 @@
                            <nil key="highlightedColor"/>
                        </label>
                        <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="2" translatesAutoresizingMaskIntoConstraints="NO" id="8n8-uF-nCb">
                            <rect key="frame" x="52.666666666666657" y="19.666666666666629" width="57.333333333333343" height="36"/>
                            <rect key="frame" x="52.666666666666657" y="10" width="57.333333333333343" height="55"/>
                            <subviews>
                                <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="会员价:" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="hAD-m3-5Ns">
                                    <rect key="frame" x="0.0" y="0.0" width="57.333333333333336" height="17"/>
@@ -389,8 +401,14 @@
                                    <color key="textColor" red="0.2470588235" green="0.2470588235" blue="0.2470588235" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
                                    <nil key="highlightedColor"/>
                                </label>
                                <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="玩湃币:" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="AuS-Gc-geQ">
                                <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="会员价:" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="55J-SL-KdW">
                                    <rect key="frame" x="0.0" y="19" width="57.333333333333336" height="17"/>
                                    <fontDescription key="fontDescription" type="system" weight="semibold" pointSize="14"/>
                                    <color key="textColor" red="0.2470588235" green="0.2470588235" blue="0.2470588235" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
                                    <nil key="highlightedColor"/>
                                </label>
                                <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="玩湃币:" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="AuS-Gc-geQ">
                                    <rect key="frame" x="0.0" y="38" width="57.333333333333336" height="17"/>
                                    <fontDescription key="fontDescription" type="system" weight="semibold" pointSize="14"/>
                                    <color key="textColor" red="0.2470588235" green="0.2470588235" blue="0.2470588235" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
                                    <nil key="highlightedColor"/>
@@ -457,7 +475,7 @@
    <resources>
        <image name="btn_add_1" width="16" height="16"/>
        <namedColor name="FE6E0D">
            <color red="0.99599999189376831" green="0.4309999942779541" blue="0.050999999046325684" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
            <color red="0.99199998378753662" green="0.53299999237060547" blue="0.0080000003799796104" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
        </namedColor>
        <systemColor name="systemBackgroundColor">
            <color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
WanPai/Root/Course/VC/CourseDetailVC.swift
@@ -49,28 +49,33 @@
            }
            label_price.text = detailModel.list.first!.paymentPrice.currency()
            if let subM = detailModel.list.first {
                label_originPrice.isHidden = subM.originalPrice == 0
                label_coin.isHidden = subM.playPaiCoin == 0
                label_vipPrice.isHidden = subM.vipPrice == 0
                label_originPrice.isHidden = subM.originalPrice == nil
                label_coin.isHidden = subM.playPaiCoin == nil
                label_vipPrice.isHidden = subM.vipPrice == nil
                //原价
                let attribute = AttributedStringbuilder.build().add(string: subM.originalPrice.currency(), withFont: UIFont.systemFont(ofSize: 16, weight: .semibold), withColor: UIColor(hexStr: "#3F3F3F").withAlphaComponent(0.58)).underLine(color: UIColor(hexStr: "#3F3F3F").withAlphaComponent(0.58))
                label_originPrice.attributedText = attribute.mutableAttributedString
                    //原价
                if let originPrice =  subM.originalPrice{
                    let attribute = AttributedStringbuilder.build().add(string: originPrice.currency(), withFont: UIFont.systemFont(ofSize: 16, weight: .semibold), withColor: UIColor(hexStr: "#3F3F3F").withAlphaComponent(0.58)).underLine(color: UIColor(hexStr: "#3F3F3F").withAlphaComponent(0.58))
                    label_originPrice.attributedText = attribute.mutableAttributedString
                }
                //玩湃币
                let coinAttribute = AttributedStringbuilder.build()
                    .add(string: "玩湃币:", withFont: UIFont.systemFont(ofSize: 14, weight: .semibold), withColor: UIColor(hexStr: "#3F3F3F"))
                    .add(string: "\(subM.playPaiCoin)币", withFont: UIFont.systemFont(ofSize: 14, weight: .semibold), withColor: UIColor(hexStr: "#F21313"))
                label_coin.attributedText = coinAttribute.mutableAttributedString
                    //玩湃币
                if let paiCoin = subM.playPaiCoin{
                    let coinAttribute = AttributedStringbuilder.build()
                        .add(string: "玩湃币:", withFont: UIFont.systemFont(ofSize: 14, weight: .semibold), withColor: UIColor(hexStr: "#3F3F3F"))
                        .add(string: "\(paiCoin)币", withFont: UIFont.systemFont(ofSize: 14, weight: .semibold), withColor: UIColor(hexStr: "#F21313"))
                    label_coin.attributedText = coinAttribute.mutableAttributedString
                }
                //会员价
                let vipAttribute = AttributedStringbuilder.build()
                    .add(string: "会员价:", withFont: UIFont.systemFont(ofSize: 14, weight: .semibold), withColor: UIColor(hexStr: "#3F3F3F"))
                    .add(string: subM.vipPrice.currency(), withFont: UIFont.systemFont(ofSize: 14, weight: .semibold), withColor: UIColor(hexStr: "#F21313"))
                label_vipPrice.attributedText = vipAttribute.mutableAttributedString
                if let vipPrice = subM.vipPrice{
                    let vipAttribute = AttributedStringbuilder.build()
                        .add(string: "会员价:", withFont: UIFont.systemFont(ofSize: 14, weight: .semibold), withColor: UIColor(hexStr: "#3F3F3F"))
                        .add(string: vipPrice.currency(), withFont: UIFont.systemFont(ofSize: 14, weight: .semibold), withColor: UIColor(hexStr: "#F21313"))
                    label_vipPrice.attributedText = vipAttribute.mutableAttributedString
                }
            }
@@ -103,7 +108,7 @@
    }
    @IBAction func applyAction(_ sender: UIButton) {
         let vc = CourseDetailApplyVC()
         let vc = CourseDetailApplyVC(detailModel: detailModel)
        push(vc: vc)
    }
    
WanPai/Root/Course/VC/StudentCourseDetailVC.swift
@@ -30,7 +30,7 @@
        title = "课时详情"
        headView.renewalClouse = { [weak self] () in
            let vc = CourseDetailApplyVC()
            let vc = CourseDetailApplyVC(detailModel: CourseDetailModel())
            self?.push(vc: vc)
        }
    }
WanPai/Root/Home/VC/HomeVC.swift
@@ -8,13 +8,22 @@
import UIKit
import JQTools
import RxSwift
import RxCocoa
import SDWebImage
    //http://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4
    //http://vjs.zencdn.net/v/oceans.mp4
    //https://media.w3.org/2010/05/sintel/trailer.mp4
class HomeVC: BaseVC,Refreshable{
class HomeViewModel:RefreshModel<HomeStoreConfigModel>{
    let storeId = BehaviorRelay<Int>(value: 0)
    override func api() -> (Observable<BaseResponse<[HomeStoreConfigModel]>>)? {
        return Services.homeStoreConfig(storeId: 1)
    }
}
class HomeVC: BaseVC{
    @IBOutlet weak var collectionView: BaseCollectionView!
    @IBOutlet weak var label_vipInfo: UILabel!
@@ -22,9 +31,11 @@
    @IBOutlet weak var label_store: UILabel!
    private var items = Array<HomeStoreConfigModel>()
    private var storeModel:HomeStoreModel?
    var viewModel = HomeViewModel()
    override func viewDidLoad() {
        super.viewDidLoad()
        viewModel.configure(collectionView,needMore: false)
        locationTool.startLocation { [weak self] local in
            locationTool.stopLocation()
@@ -35,35 +46,40 @@
            self?.getStoreInfo()
        }
        refreshStatusBind(to: collectionView,header: { [weak self] () in
            guard let weakSelf = self,weakSelf.storeModel != nil else {
                self?.refreshStatus.onNext(.endHeaderRefresh);return
            }
            Services.homeStoreConfig(storeId: weakSelf.storeModel!.storeId).subscribe(onNext: {data in
                if let models = data.data{
                    self?.items = models
                    let group = DispatchGroup()
        viewModel.dataSource.subscribe(onNext: {[weak self] data in
            guard let weakSelf = self else { return }
            guard data.count != 0 else {return}
            weakSelf.items = data
            let group = DispatchGroup()
                    for m in models{
                        let queue = DispatchQueue(label: "1")
                        queue.async(group: group) {
                            group.enter()
                            SDWebImageDownloader.shared.downloadImage(with: URL(string: m.backgroundImage)) { image, data, error, status in
                                if let i = image{
                                    m.radio = i.size.width / i.size.height
                                }
                                group.leave()
                            }
            for m in weakSelf.items{
                let queue = DispatchQueue(label: "1")
                queue.async(group: group) {
                    group.enter()
                    SDWebImageDownloader.shared.downloadImage(with: URL(string: m.backgroundImage),options: .scaleDownLargeImages,progress: nil) { image, data, error, status in
                        if let i = image{
                            m.radio = i.size.width / i.size.height
                        }
                    }
                    group.notify(queue: .main){
                        self?.refreshStatus.onNext(.endHeaderRefresh)
                        self?.collectionView.reloadData()
                        group.leave()
                    }
                }
            }).disposed(by: weakSelf.disposeBag)
        })
            }
            group.notify(queue: .main){
                //重置Layout,不然不更新
                self!.layout = WaterFallFlowLayout()
                self!.layout.cols = 2
                self!.layout.sectionInset = UIEdgeInsets(top: 14, left: 34, bottom: 14, right: 34)
                self!.layout.delegate = self
                self!.collectionView.collectionViewLayout = self!.layout
                self!.collectionView.reloadData()
            }
        }).disposed(by: disposeBag)
    }
    override func setRx() {
    }
    
    override func setUI() {
@@ -90,8 +106,8 @@
                self?.label_vipInfo.text = text
                if m.storeId != 0{
                    self?.storeModel = m
                    self?.refreshStatus.onNext(.beingHeaderRefresh)
                    self?.viewModel.storeId.accept(m.storeId)
                    self?.viewModel.beginRefresh()
                }else{
                    self?.defaultData()
                }
WanPai/Root/Other/CCell/Common_1_CCell.swift
@@ -10,9 +10,22 @@
class Common_1_CCell: UICollectionViewCell {
    @IBOutlet weak var label_content: UILabel!
    var courseDetailListModel:CourseDetailListModel?{
        didSet{
            if let m = courseDetailListModel{
                label_content.text = "\(m.classHours)课时"
                label_content.backgroundColor = isSelected ? Def_ThemeColor : .white
                label_content.textColor = isSelected ? .white : UIColor(hexStr: "#898989")
                label_content.borderWidth = isSelected ? 0:1
            }
        }
    }
    override func awakeFromNib() {
        super.awakeFromNib()
        label_content.borderColor = UIColor(hexStr: "#898989")
        label_content.cornerRadius = 4
    }
}
WanPai/Root/Other/TCell/StudentInfoTCell.swift
@@ -9,6 +9,17 @@
class StudentInfoTCell: UITableViewCell {
    var studentModel:CourseDetailStudentModel?{
        didSet{
            label_name.text = studentModel?.name ?? "--"
            label_phone.text = studentModel?.phone ?? "--"
            label_age.text = "\(studentModel?.age ?? 0)岁"
        }
    }
    @IBOutlet weak var label_name: UILabel!
    @IBOutlet weak var label_phone: UILabel!
    @IBOutlet weak var label_age: UILabel!
    override func awakeFromNib() {
        super.awakeFromNib()
        selectionStyle = .none
WanPai/Root/Other/TCell/StudentInfoTCell.xib
@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="21507" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="21701" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
    <device id="retina6_12" orientation="portrait" appearance="light"/>
    <dependencies>
        <deployment identifier="iOS"/>
        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21505"/>
        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21679"/>
        <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
    </dependencies>
    <objects>
@@ -109,6 +109,11 @@
                    <constraint firstItem="wux-IQ-y3O" firstAttribute="top" secondItem="H2p-sc-9uM" secondAttribute="top" constant="7" id="d2J-Ta-zpG"/>
                </constraints>
            </tableViewCellContentView>
            <connections>
                <outlet property="label_age" destination="o0m-fp-glP" id="KJW-vV-H6Z"/>
                <outlet property="label_name" destination="0TW-3R-Kd5" id="Rxm-mg-OvM"/>
                <outlet property="label_phone" destination="A9b-WE-sPt" id="obG-aM-y0V"/>
            </connections>
            <point key="canvasLocation" x="74.809160305343511" y="45.422535211267608"/>
        </tableViewCell>
    </objects>
WanPai/Root/Welfare/VC/WelfareWeeklyDetailVC.swift
@@ -15,7 +15,7 @@
        
    }
    @IBAction func applyAction(_ sender: UIButton) {
        let vc = CourseDetailApplyVC()
        let vc = CourseDetailApplyVC(detailModel: CourseDetailModel())
        push(vc: vc)
    }
}
WanPai/ViewModel/RefreshModel.swift
New file
@@ -0,0 +1,188 @@
    //
    //  RefreshModel.swift
    //  WanPai
    //
    //  Created by 无故事王国 on 2023/7/11.
    //
import UIKit
import MJRefresh
import RxSwift
import HandyJSON
import RxRelay
enum RefreshState {
    case refreshing
    case completedRefresh
    case moreLoading
    case completedLoad
    case completedLoadWithNoMoreData
}
protocol RefreshModelProctol {
    associatedtype T:HandyJSON
    func api()->(Observable<BaseResponse<[T]>>)?
}
class RefreshModel<T:HandyJSON>:RefreshModelProctol{
    func api() -> (RxSwift.Observable<BaseResponse<[T]>>)? {
        return nil
    }
    let disposeBag = DisposeBag()
    enum RefreshType {
        case refresh,load
    }
    private var handle:UIScrollView!
    lazy var refreshSubject = PublishSubject<RefreshState>()
    var page:Int = 0
    var pageSize:Int = 20
    lazy var dataSource = BehaviorRelay<[T]>(value: [])
    func configure(_ scrollView:UITableView,needMore:Bool = true){
        scrollView.mj_header = CustomRefreshHeaer.refreshing(with: refreshData())
        if needMore{
            scrollView.mj_footer = CustomRefreshFooter.refreshing(with: loadMoreData())
        }
        refreshSubject.bind(to: scrollView.rx.handlestatus()).disposed(by: disposeBag)
        handle = scrollView
    }
    func configure(_ scrollView:UICollectionView,needMore:Bool = true){
        scrollView.mj_header = CustomRefreshHeaer.refreshing(with: refreshData())
        if needMore{
            scrollView.mj_footer = CustomRefreshFooter.refreshing(with: loadMoreData())
        }
        refreshSubject.bind(to: scrollView.rx.handlestatus()).disposed(by: disposeBag)
        handle = scrollView
    }
    func beginRefresh(){
        handle.mj_header?.beginRefreshing()
    }
    func refreshData() ->(()->Void) {
        return {self.request(status: .refresh)}
    }
    func loadMoreData() ->(()->Void) {
        return {self.request(status: .load)}
    }
    func request(status:RefreshType){
        switch status {
            case .refresh:
                self.page = 1
                self.refreshSubject.onNext(.refreshing)
            case .load:
                self.page += 1
                self.refreshSubject.onNext(.moreLoading)
        }
        api()?.subscribe(onNext: { data in
            if let datas = data.data,datas.count > 0{
                switch status{
                    case .refresh:
                        self.dataSource.accept(datas)
                        self.refreshSubject.onNext(.completedRefresh)
                    case .load:
                        self.dataSource.accept(self.dataSource.value + datas)
                        self.refreshSubject.onNext(.completedLoad)
                }
            }else{
                self.refreshSubject.onNext(.completedLoadWithNoMoreData)
            }
        }, onError: { error in
            self.refreshSubject.onNext(.completedLoad)
        }).disposed(by: disposeBag)
    }
}
class CustomRefreshHeaer:MJRefreshNormalHeader{
    static func refreshing(with refreshingBlock: @escaping MJRefreshComponentAction) -> MJRefreshNormalHeader? {
        let refreshHeader = MJRefreshNormalHeader(refreshingBlock: refreshingBlock)
        return refreshHeader
    }
}
class CustomRefreshFooter:MJRefreshAutoNormalFooter{
    static func refreshing(with refreshingBlock: @escaping MJRefreshComponentAction) -> MJRefreshAutoNormalFooter? {
        let refrehFooter = MJRefreshAutoNormalFooter(refreshingBlock: refreshingBlock)
        return refrehFooter
    }
}
extension Reactive where Base : UITableView {
    func handlestatus() -> Binder<RefreshState> {
        return Binder(self.base) { (tableView, status) in
            switch status {
                case .moreLoading:
                    self.base.mj_footer?.beginRefreshing()
                case .refreshing:
                    self.base.reloadData()
                    self.base.mj_footer?.resetNoMoreData()
                    self.base.mj_header?.endRefreshing()
                case .completedLoadWithNoMoreData:
                    DispatchQueue.main.async {
                        self.base.reloadData()
                    }
                    self.base.mj_footer?.endRefreshingWithNoMoreData()
                case .completedLoad:
                    DispatchQueue.main.async {
                        self.base.reloadData()
                    }
                    self.base.mj_footer?.endRefreshing()
                default:
                    DispatchQueue.main.async {
                        self.base.reloadData()
                    }
                    self.base.mj_header?.endRefreshing()
                    self.base.mj_footer?.endRefreshing()
            }
        }
    }
}
extension Reactive where Base : UICollectionView {
    func handlestatus() -> Binder<RefreshState> {
        return Binder(self.base) { (tableView, status) in
            switch status {
                case .moreLoading:
                    self.base.mj_footer?.beginRefreshing()
                case .refreshing:
                    self.base.reloadData()
                    self.base.mj_footer?.resetNoMoreData()
                    self.base.mj_header?.endRefreshing()
                case .completedLoadWithNoMoreData:
                    DispatchQueue.main.async {
                        self.base.reloadData()
                    }
                    self.base.mj_footer?.endRefreshingWithNoMoreData()
                case .completedLoad:
                    DispatchQueue.main.async {
                        self.base.reloadData()
                    }
                    self.base.mj_footer?.endRefreshing()
                default:
                    DispatchQueue.main.async {
                        self.base.reloadData()
                    }
                    self.base.mj_header?.endRefreshing()
                    self.base.mj_footer?.endRefreshing()
            }
        }
    }
}