墜入鏈?zhǔn)骄幊痰幕脴防?-用RxSwift仿寫知乎日報(bào)

用過無數(shù)的三方庫锌奴,卻仍舊寫不好代碼。以前總會(huì)有人問:你用過最好的三方庫是什么憾股?那個(gè)時(shí)候總是會(huì)猶豫半天鹿蜀,到底是哪一個(gè)呢?好像都還可以耶荔燎,直到后來遇到RxSwift耻姥,哇,簡直打開了新世界的大門∮凶桑現(xiàn)在我會(huì)毫不猶豫推薦它,雖然學(xué)習(xí)曲線有點(diǎn)陡峭蒸健,但是一旦你習(xí)慣上它座享,必深陷于其中無法自拔婉商。

初入RxSwift

在公司項(xiàng)目進(jìn)入版本迭代的時(shí)期,總覺得應(yīng)該學(xué)點(diǎn)什么渣叛,不然讓拍在沙灘上怎么辦丈秩?在學(xué)習(xí)swift3一段時(shí)間后,邂逅了響應(yīng)式編程方式淳衙,看了一下相關(guān)文章蘑秽,毫不猶豫跳入RxSwift的坑中,其中險(xiǎn)些放棄箫攀,還好堅(jiān)持下來了肠牲,現(xiàn)在也算入了個(gè)門。當(dāng)然只看看理論知識(shí)點(diǎn)靴跛,光紙上談兵是不行的缀雳,所以選擇仿寫知日報(bào)的方式來深化一下知識(shí)。

項(xiàng)目實(shí)戰(zhàn)

整個(gè)項(xiàng)目持續(xù)的大概兩周敷硅,遇到不少問題,畢竟不管對于Swift還是RxSwift來說墓赴,我大概都只是個(gè)新手竞膳。

網(wǎng)絡(luò)請求 Moya + RxSwift
  • API: 項(xiàng)目的開始當(dāng)然是看看有沒有API呀,這里要感謝這位通過非正常手段獲取API的同學(xué)诫硕,為我們總結(jié)了完整的知乎日報(bào)-API-分析坦辟,我也無私地奉獻(xiàn)了star,略表感謝章办!
  • Alamofire: Swift版的AFNetworking锉走。
  • Moya: 是 Artsy 團(tuán)隊(duì)的 Ash Furrow 主導(dǎo)開發(fā)的一個(gè)網(wǎng)絡(luò)抽象層庫。它在 Alamofire 基礎(chǔ)上提供了一系列簡單的抽象接口藕届,讓客戶端代碼不用去直接調(diào)用 Alamofire挪蹭,也不用去關(guān)心 NSURLSession。同時(shí)提供了很多實(shí)用的功能休偶,包括對RxSwift的良好擴(kuò)展梁厉。
  • HandyJSON: 是一個(gè)用于Swift語言中的JSON序列化/反序列化庫。與其他流行的Swift JSON庫相比,HandyJSON的特點(diǎn)是词顾,它支持純swift類八秃,使用也簡單。它反序列化時(shí)(把JSON轉(zhuǎn)換為Model)不要求Model從NSObject繼承(因?yàn)樗皇腔贙VC機(jī)制)肉盹,也不要求你為Model定義一個(gè)Mapping函數(shù)昔驱。只要你定義好Model類,聲明它服從HandyJSON協(xié)議上忍,HandyJSON就能自行以各個(gè)屬性的屬性名為Key骤肛,從JSON串中解析值。HandyJSON目前依賴于從Swift Runtime源碼中推斷的內(nèi)存規(guī)則窍蓝,任何變動(dòng)我們將隨時(shí)跟進(jìn)腋颠。
  • RxSwift: 響應(yīng)式編程三方庫。這里主要處理網(wǎng)絡(luò)請求時(shí)的各種回調(diào)和異步線程它抱。

最終實(shí)現(xiàn)效果:

let provider = RxMoyaProvider<ApiManager>()
provider                                      //moya網(wǎng)絡(luò)請求的manager
    .request(.getNewsList)                    //各種請求以枚舉的形式調(diào)用
    .mapModel(listModel.self)                 //JOSN->Model
    .subscribe(onNext: { (model) in
        print(model)                          //請求數(shù)據(jù)回調(diào)秕豫,處理數(shù)據(jù)
    })
    .addDisposableTo(dispose)                 //資源回收

API枚舉:

enum ApiManager {
    case getLaunchImg
    case getNewsList
    case getMoreNews(String)
    case getThemeList
    case getThemeDesc(Int)
    case getNewsDesc(Int)
}

由于Moya沒有支持HandyJSON擴(kuò)展,這里我自己實(shí)現(xiàn)了此擴(kuò)展:

extension ObservableType where E == Response {
    public func mapModel<T: HandyJSON>(_ type: T.Type) -> Observable<T> {
        return flatMap { response -> Observable<T> in
            return Observable.just(response.mapModel(T.self))
        }
    }
}

extension Response {
    func mapModel<T: HandyJSON>(_ type: T.Type) -> T {
        let jsonString = String.init(data: data, encoding: .utf8)
        return JSONDeserializer<T>.deserializeFrom(json: jsonString)!
    }
}

只要Model遵循HandyJSON協(xié)議观蓄,就能很優(yōu)雅的快速實(shí)現(xiàn)JSON->Model混移,包括嵌套解析:

struct listModel: HandyJSON {
    var date: String?
    var stories: [storyModel]?
    var top_stories: [storyModel]?
}

struct storyModel: HandyJSON {
    var ga_prefix: String?
    var id: Int?
    var images: [String]? //list_stories
    var title: String?
    var type: Int?
    var image: String? //top_stories
    var multipic = false
}

可以說,這是迄今為止我最滿意的網(wǎng)絡(luò)請求封裝侮穿,以后都可以愉快處理請求啦??

數(shù)據(jù)呈現(xiàn)

數(shù)據(jù)請求處理好了歌径,就該綁定視圖顯示出來了,這里就是RxSwift的拿手好戲了亲茅。下面我們先看最簡單的展現(xiàn):

    let provider = RxMoyaProvider<ApiManager>()
    let dispose = DisposeBag()
    let themeArr = Variable([ThemeModel]())
        
        //請求數(shù)據(jù)
        provider
            .request(.getThemeList)
            .mapModel(ThemeResponseModel.self)
            .subscribe(onNext: { (model) in
                self.themeArr.value = model.others!
            })
            .addDisposableTo(dispose)
        
        //綁定視圖
        themeArr
            .asObservable()
            .bindTo(tableView.rx.items(cellIdentifier: "ThemeTableViewCell", cellType: ThemeTableViewCell.self)) {
                row, model, cell in
                cell.name.text = model.name
                cell.homeIcon.isHidden = row == 0 ? false : true
                cell.nameLeft.constant = row == 0 ? 50 : 15
        }
            .addDisposableTo(dispose)
      
       //響應(yīng)視圖   
        tableView.rx
            .modelSelected(ThemeModel.self)
            .subscribe(onNext: { (model) in
                self.showView = false
                self.showThemeVC(model)
            })
            .addDisposableTo(dispose)

這樣簡單的幾行代碼就完成網(wǎng)絡(luò)請求數(shù)據(jù)展現(xiàn)以及用戶響應(yīng)一系列流程回铛,什么代理,擴(kuò)展都不用寫了克锣,減少了一半以上的代碼茵肃,是不是看著就覺得爽炸了!我們再看看復(fù)雜一點(diǎn)的袭祟,分組tableview:

        let dataSource = RxTableViewSectionedReloadDataSource<SectionModel<String, storyModel>>()
        let dispose = DisposeBag()

        dataSource.configureCell = { (dataSource, tv, indexPath, model) in
            let cell = tv.dequeueReusableCell(withIdentifier: "ListTableViewCell") as! ListTableViewCell
            cell.title.text = model.title
            cell.img.kf.setImage(with: URL.init(string: (model.images?.first)!))
            cell.morepicImg.isHidden = !model.multipic
            return cell
        }
        
        dataArr
            .asObservable()
            .bindTo(tableView.rx.items(dataSource: dataSource))
            .addDisposableTo(dispose)
        
        tableView.rx
            .modelSelected(storyModel.self)
            .subscribe(onNext: { (model) in
                self.tableView.deselectRow(at: self.tableView.indexPathForSelectedRow!, animated: true)
                let detailVc = DetailViewController()
                detailVc.id = model.id!
                self.navigationController?.pushViewController(detailVc, animated: true)
            })
            .addDisposableTo(dispose)

其實(shí)也很簡單验残,就是需要綁定SectionModel,當(dāng)然你也可以自定義SectionModel來分組展示巾乳,上面的代碼都在項(xiàng)目篩選出來的您没,具體實(shí)現(xiàn)可以看文末項(xiàng)目鏈接。

項(xiàng)目難點(diǎn)
1. 菜單欄與主頁面的切換
menuShow.gif

由于導(dǎo)航欄一開始用的原生的(其實(shí)應(yīng)該自定義胆绊,因?yàn)楹竺嫔婕暗胶芏鄬?dǎo)航欄問題)氨鹏,所以左右平移的時(shí)候要把導(dǎo)航欄一起移動(dòng),所以遇到了一點(diǎn)問題压状,后來查找相關(guān)資料后解決了此問題:

    func showMenu() {
        let view = UIApplication.shared.keyWindow?.subviews.first
        let menuView = UIApplication.shared.keyWindow?.subviews.last
        UIApplication.shared.keyWindow?.bringSubview(toFront: (UIApplication.shared.keyWindow?.subviews[1])!)
        UIView.animate(withDuration: 0.5, animations: { 
            view?.transform = CGAffineTransform.init(translationX: 225, y: 0)
            menuView?.transform = (view?.transform)!
        })
    }
    
    func dismissMenu() {
        let view = UIApplication.shared.keyWindow?.subviews.first
        let menuView = UIApplication.shared.keyWindow?.subviews.last
        UIApplication.shared.keyWindow?.bringSubview(toFront: (UIApplication.shared.keyWindow?.subviews[1])!)
        UIView.animate(withDuration: 0.5, animations: {
            view?.transform = CGAffineTransform.init(translationX: 0, y: 0)
            menuView?.transform = (view?.transform)!
        })
    }

菜單欄的顯示和隱藏需要配合手勢仆抵,研究官方知乎日報(bào)App后,發(fā)現(xiàn)存在輕掃和拖拽滑動(dòng)兩個(gè)手勢,相對應(yīng)UIPanGestureRecognizer和UISwipeGestureRecognizer肢础,當(dāng)把這兩個(gè)視圖分別加在視圖上的時(shí)候还栓,只會(huì)響應(yīng)一個(gè)手勢碌廓,后來設(shè)置UIGestureRecognizerDelegate后避免了這個(gè)問題:

extension HomeViewController: UIGestureRecognizerDelegate {
    //是否允許手勢識(shí)別器同時(shí)識(shí)別兩個(gè)手勢
    func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
        return true
    }
}

本以為就此解決了問題传轰,但是實(shí)際操作起來,手機(jī)很難區(qū)分這個(gè)兩個(gè)手勢谷婆,經(jīng)常會(huì)搞錯(cuò)慨蛙,本來想拖拽滑動(dòng)結(jié)果系統(tǒng)識(shí)別為了輕掃手勢,體驗(yàn)效果很差纪挎,那怎么辦呢期贫?后來終于找到一種可行方案:只在視圖上添加UIPanGestureRecognizer,以手指操作時(shí)間來區(qū)分是輕掃還是拖拽滑動(dòng)

    func panGesture(pan: UIPanGestureRecognizer) {
        let xoff = pan.translation(in: view).x
        if pan.state == .began {
            beganDate = Date()
        }
        if pan.state == .ended {
            endDate = Date()
            //區(qū)分是輕掃還是滑動(dòng)
            if endDate! < beganDate! + 150000000.nanoseconds {
                if xoff > 0 {
                    showView = true
                } else {
                    showView = false
                }
                return
            }
        }
        //滑動(dòng)范圍以及滑動(dòng)結(jié)束后需要show還是dismiss
        if (0 < xoff && xoff <= 225 && !showView) || (0 > xoff && xoff >= -225 && showView) {
            if pan.translation(in: view).x > 0 {
                moveMenu(pan.translation(in: view).x)
            } else {
                moveMenu(225 + pan.translation(in: view).x)
            }
            if pan.state == .ended {
                if showView {
                    if pan.translation(in: view).x < -175 {
                        showView = false
                    } else {
                        showView = true
                    }
                } else {
                    if pan.translation(in: view).x > 50 {
                        showView = true
                    } else {
                        showView = false
                    }
                }
            }
        }
    }

菜單欄與主頁面的切換中還有一個(gè)不好處理的點(diǎn)异袄,當(dāng)選中菜單欄某個(gè)主題后通砍,要推出一個(gè)主題日報(bào)列表,與首頁不同屬于一個(gè)UINavigationController烤蜕,那怎么從一個(gè)UINavigationController到另一個(gè)UINavigationController呢封孙?試了好幾種方式來切換,始終達(dá)不到官方效果讽营,忙碌了一天虎忌,最后靈光一現(xiàn)(也可能是我太蠢??)平常不是都用UITabBarController來切換UINavigationController?橱鹏!真的好簡單膜蠢,隱藏掉tabbar就好,幾句代碼就完美解決了這個(gè)場景切換問題:

    func showThemeVC(_ model: ThemeModel) {
        if model.id == nil {
            bindtoNav?.selectedIndex = 0
        } else {
            bindtoNav?.selectedIndex = 1
        }
    }

如果你有更好的切換方法請聯(lián)系我莉兰,愿意請你喝咖啡??

2. 文章的快速切換
newsChange.gif

文章詳情是用UIWebView加載html數(shù)據(jù)來展現(xiàn)的挑围,這里我自定義class DetailWebView: UIWebView,以便于兩個(gè)文章詳情的切換糖荒,用于顯示文章詳情的DetailViewController包含兩個(gè)DetailWebView杉辙,一個(gè)webview用于展示當(dāng)前頁面,另一個(gè)previousWeb放在屏幕外準(zhǔn)備隨時(shí)切換文章寂嘉,當(dāng)發(fā)生切換文章時(shí)奏瞬,動(dòng)畫呈現(xiàn)previousWeb,并在后續(xù)移除在屏幕外webview泉孩,把previousWeb作為新的webview硼端,同時(shí)生成新的previousWeb

    //切換文章詳情
    func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
        if scrollView.contentOffset.y <= -60 {
            if previousId > 0 {
                previousWeb.frame = CGRect.init(x: 0, y: -screenH, width: screenW, height: screenH)
                UIView.animate(withDuration: 0.3, animations: {
                    self.webview.transform = CGAffineTransform.init(translationX: 0, y: screenH)
                    self.previousWeb.transform = CGAffineTransform.init(translationX: 0, y: screenH)
                }, completion: { (state) in
                    if state { self.changeWebview(self.previousId) }
                })
            }
        }
        if scrollView.contentOffset.y - 50 + screenH >= scrollView.contentSize.height {
            if nextId > 0 {
                previousWeb.frame = CGRect.init(x: 0, y: screenH, width: screenW, height: screenH)
                UIView.animate(withDuration: 0.3, animations: {
                    self.previousWeb.transform = CGAffineTransform.init(translationX: 0, y: -screenH)
                    self.webview.transform = CGAffineTransform.init(translationX: 0, y: -screenH)
                }, completion: { (state) in
                    if state { self.changeWebview(self.nextId) }
                })
            }
        }
    }

    //切換之后后續(xù)處理
    func changeWebview(_ showID: Int) {
        webview.removeFromSuperview()
        previousWeb.scrollView.delegate = self
        previousWeb.delegate = self
        webview = previousWeb
        id = showID
        setUI()
        previousWeb = DetailWebView.init(frame: CGRect.init(x: 0, y: -screenH, width: screenW, height: screenH))
        view.addSubview(previousWeb)
        scrollViewDidScroll(webview.scrollView)
    }
3.首頁刷新

Swift版的刷新控件三方還沒找比較好的,一度打算自己封裝一個(gè)寓搬,但是一直拖著珍昨,??以后應(yīng)該會(huì)寫。
知乎日報(bào)的刷新控件與一般放在tableview上不同,它應(yīng)該是放在導(dǎo)航欄上面镣典,配合tableview來實(shí)現(xiàn)刷新兔毙,這也是前面為什么說導(dǎo)航欄要自定義的原因之一,因?yàn)橐呀?jīng)用了原生的導(dǎo)航欄兄春,只好巧妙(偷懶)加在了view上澎剥,其實(shí)這個(gè)刷新就是一個(gè)畫圓圈的過程,下面看看自定義的RefreshView:

class RefreshView: UIView {

    let circleLayer = CAShapeLayer()

    let indicatorView = UIActivityIndicatorView().then {
        $0.frame = CGRect(x: 0, y: 0, width: 16, height: 16)
    }
    
    fileprivate var refreshing = false
    fileprivate var endRef = false
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        creatCircleLayer()
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        circleLayer.position = CGPoint(x: frame.width/2, y: frame.height/2)
        indicatorView.center = CGPoint(x: frame.width/2, y: frame.height/2)
    }
    
    func creatCircleLayer() {
        circleLayer.path = UIBezierPath(arcCenter: CGPoint(x: 8, y: 8),
                               radius: 8,
                               startAngle: CGFloat(M_PI_2),
                               endAngle: CGFloat(M_PI_2 + 2*M_PI),
                               clockwise: true).cgPath
        circleLayer.strokeColor = UIColor.white.cgColor
        circleLayer.fillColor = UIColor.clear.cgColor
        circleLayer.strokeStart = 0.0
        circleLayer.strokeEnd = 0.0
        circleLayer.lineWidth = 1.0
        circleLayer.lineCap = kCALineCapRound
        circleLayer.bounds = CGRect(x: 0, y: 0, width: 16, height: 16)
        circleLayer.anchorPoint = CGPoint(x: 0.5, y: 0.5)
        layer.addSublayer(circleLayer)
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

}

extension RefreshView {
    //向下拖拽視圖準(zhǔn)備刷新的過程會(huì)響應(yīng)
    func pullToRefresh(progress: CGFloat) {
        circleLayer.strokeEnd = progress
    }
    //開始刷新
    func beginRefresh(begin: @escaping () -> Void) {
        if refreshing {
            //防止刷新未結(jié)束又開始請求刷新
            return
        }
        refreshing = true
        circleLayer.removeFromSuperlayer()
        addSubview(indicatorView)
        indicatorView.startAnimating()
        begin()
    }
    //結(jié)束刷新
    func endRefresh() {
        refreshing = false
        indicatorView.stopAnimating()
        indicatorView.removeFromSuperview()
    }
    //重制刷新控件
    func resetLayer() {
        DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.5) {
            self.creatCircleLayer()
        }
    }
    
}

注意事項(xiàng)

  • 項(xiàng)目中關(guān)于時(shí)間的處理用的是 SwiftDate
  • .then 語法用的是 Then赶舆,小而妙哑姚,很喜歡

總結(jié)

小生才疏學(xué)淺,未有編程天賦芜茵,難免有許多謬誤紕漏之處叙量,各位看官當(dāng)看且看,若有任何問題都可以提出九串,愿接受各種批評建議绞佩。要是覺得這篇文章稍有用處,可以給個(gè)star猪钮,十分感激品山。

項(xiàng)目源碼:ZhiHu-RxSwift

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市躬贡,隨后出現(xiàn)的幾起案子谆奥,更是在濱河造成了極大的恐慌,老刑警劉巖拂玻,帶你破解...
    沈念sama閱讀 218,682評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件酸些,死亡現(xiàn)場離奇詭異,居然都是意外死亡檐蚜,警方通過查閱死者的電腦和手機(jī)魄懂,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來闯第,“玉大人市栗,你說我怎么就攤上這事】榷蹋” “怎么了填帽?”我有些...
    開封第一講書人閱讀 165,083評論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長咙好。 經(jīng)常有香客問我篡腌,道長,這世上最難降的妖魔是什么勾效? 我笑而不...
    開封第一講書人閱讀 58,763評論 1 295
  • 正文 為了忘掉前任嘹悼,我火速辦了婚禮叛甫,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘杨伙。我一直安慰自己其监,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,785評論 6 392
  • 文/花漫 我一把揭開白布限匣。 她就那樣靜靜地躺著抖苦,像睡著了一般。 火紅的嫁衣襯著肌膚如雪膛腐。 梳的紋絲不亂的頭發(fā)上睛约,一...
    開封第一講書人閱讀 51,624評論 1 305
  • 那天,我揣著相機(jī)與錄音哲身,去河邊找鬼。 笑死贸伐,一個(gè)胖子當(dāng)著我的面吹牛勘天,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播捉邢,決...
    沈念sama閱讀 40,358評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼脯丝,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了伏伐?” 一聲冷哼從身側(cè)響起宠进,我...
    開封第一講書人閱讀 39,261評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎藐翎,沒想到半個(gè)月后材蹬,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,722評論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡吝镣,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年堤器,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片末贾。...
    茶點(diǎn)故事閱讀 40,030評論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡闸溃,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出拱撵,到底是詐尸還是另有隱情辉川,我是刑警寧澤,帶...
    沈念sama閱讀 35,737評論 5 346
  • 正文 年R本政府宣布拴测,位于F島的核電站乓旗,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏昼扛。R本人自食惡果不足惜寸齐,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,360評論 3 330
  • 文/蒙蒙 一欲诺、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧渺鹦,春花似錦扰法、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,941評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至吸耿,卻和暖如春祠锣,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背咽安。 一陣腳步聲響...
    開封第一講書人閱讀 33,057評論 1 270
  • 我被黑心中介騙來泰國打工伴网, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人妆棒。 一個(gè)月前我還...
    沈念sama閱讀 48,237評論 3 371
  • 正文 我出身青樓澡腾,卻偏偏與公主長得像,于是被迫代替她去往敵國和親糕珊。 傳聞我的和親對象是個(gè)殘疾皇子动分,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,976評論 2 355

推薦閱讀更多精彩內(nèi)容