實踐 Flutter 交友功能-網(wǎng)絡緩存

本課時我會和大家一起來完善 App 的其他功能仗谆,其中包括:我的好友懊直、我的消息扒吁、系統(tǒng)設置和搜索功能。按照我們之前課時所學的技術點室囊,我們可以通過繪制組件樹+布局來實現(xiàn)雕崩,在實現(xiàn)過程中也會介紹一些新的知識點,接下來我們就分別看下這幾個功能的實現(xiàn)過程融撞。

我的好友

我們首先看下要實現(xiàn)的效果圖盼铁,如圖 1 所示。

圖 1 我的好友效果圖

根據(jù)圖 1 的效果圖懦铺,我們繪制出組件樹+布局捉貌,如圖 2 所示。

圖 2 組件樹

圖 2 很清晰地分析出了界面所轉化的組件樹,由于這里都不涉及動態(tài)組件趁窃,因此將 Text 和 Image 作為一個 card 組件即可牧挣。代碼實現(xiàn)邏輯和我們之前介紹的推薦頁面和關注頁面基本一樣,接下來我們看下“我的消息”的實現(xiàn)醒陆。

我的消息

我們先來看下“我的消息”要實現(xiàn)的界面效果瀑构,如圖 3 所示。

圖 3 我的消息界面效果

根據(jù)圖 3 的效果圖刨摩,我們繪制出組件樹+布局寺晌,如圖 4 所示。

圖 4 我的消息組件樹+布局

圖 4 就非常清晰地描述了我們整個 UI 構造:

圖 4 中的 Row-Expanded-1 和 Row-Expanded-5 代表的是使用 flex 布局澡刹,左右屏幕占比 1 比 5呻征;

圖 4 中的 first_line 代表的是圖 3 右側的用戶昵稱和時間一行;

圖 4 中的 spaceBetween 是 Row 的 mainAxisAlignment 屬性罢浇,代表的是兩端對齊陆赋,具體這部分代碼如下。

復制代碼

/// 獲取右側的首行

Widget _getFirstLine() {

? return? Row(

? ? mainAxisAlignment: MainAxisAlignment.spaceBetween,

? ? children: <Widget>[

? ? ? Text(

? ? ? ? userMessage.userInfo.nickName,

? ? ? ? style: TextStyles.commonStyle(0.8, Colors.black),

? ? ? ),

? ? ? _getMessageTimeSection(userMessage.messageTime),

? ? ],

? );

}

由于這里也沒有涉及組件的復用和動態(tài)組件嚷闭,因此這里也建議將整個組件內容設計為一個組件叫作 message_card攒岛。為了代碼維護性,可以使用類函數(shù)來封裝小組件胞锰,為后續(xù)重構抽象為通用組件做準備灾锯,例如這里我們將 first_line 設計為一個類函數(shù),如上代碼中的 _getFirstLine 函數(shù)嗅榕。

系統(tǒng)設置

接下來我們來看下“系統(tǒng)設置”這部分界面效果顺饮,如圖 5 所示。

圖 5 系統(tǒng)設置的效果

看到圖 5 的效果后凌那,其實組件設計可能不是關鍵领突。這里涉及兩個新的知識點:

在 Flutter 上怎么處理表單數(shù)據(jù);

怎么保存系統(tǒng)設置的數(shù)據(jù)案怯。

這里具體的組件樹+布局就不繪制了,我們可以將實現(xiàn)過程分為四部分:第三方庫引入澎办、通用文件存儲嘲碱、model 應用和組件應用。

第三方庫

這里我們需要使用到 Flutter 本地存儲功能局蚀,F(xiàn)lutter 本地存儲功能包含三種:shared_preferences麦锯、path_provider 文件存儲以及 sqflite。這里只介紹 path_provider 文件存儲的實現(xiàn)琅绅,其他兩個大家參照官網(wǎng)的介紹嘗試即可扶欣。使用該 path_provider 庫需要在 pubspec.yaml 中增加庫引入,然后更新本地庫。

通用文件存儲

接下來我們基于 path_provider 實現(xiàn)一個通用的文件內容存儲料祠,代碼在 github 源碼中的 util/tools/local_storage.dart 中骆捧。這里我們主要需要實現(xiàn)兩個方法,一個是文件儲存內容髓绽,一個文件讀取內容敛苇。

文件存儲

我們先來看下文件存儲的邏輯,如下代碼:

復制代碼

/// 將數(shù)據(jù)保存到文件中

static Future<void> save(String content, String filePath) async {

? final directory = await getApplicationDocumentsDirectory();

? File file = new File('${directory.path}/$filePath');

? file.writeAsString(content);

}

因為是異步獲取文件存儲路徑顺呕,因此 save 方法也需要作為異步邏輯枫攀,由于無須等待處理結果,因此返回 void株茶。上面代碼中使用了 path_provider 的 getApplicationDocumentsDirectory 的方法獲取文件存儲目錄来涨,使用 dart:io 獲取具體文件的操作句柄,最后將內容寫入文件启盛,接下來我們看下讀取的過程蹦掐。

文件讀取

讀取的過程和寫的代碼相似,首先是找到文件并獲取文件操作句柄驰徊,然后再使用文件句柄讀取文件具體內容笤闯,代碼如下:

復制代碼

/// 獲取文件數(shù)據(jù)內容

static Future<String> get(String filePath) async {

? try {

? ? final directory = await getApplicationDocumentsDirectory();

? ? File file = new File('${directory.path}/$filePath');

? ? bool exist = await file.exists();

? ? if(!exist){ // 判斷是否存在文件

? ? ? return '';

? ? }

? ? return file.readAsString();

? } catch(e) {

? ? return '';

? }

}

上面代碼增加了一個異常處理,避免讀取失敗返回錯誤數(shù)據(jù)棍厂,因此如果這里判斷異常颗味,則返回空字符串。在 catch 邏輯中是需要增加上報來監(jiān)控告警的牺弹,后續(xù)我們再來介紹這部分內容浦马。

model 應用

因為系統(tǒng)配置是一個全局狀態(tài),需要在多個頁面使用张漂,所以我們需要將系統(tǒng)數(shù)據(jù)保存在 model 中晶默,因此我們在 model 創(chuàng)建

system_config_model.dart 文件。在實現(xiàn)邏輯中航攒,需要先調用 LocalStorage 來獲取初始配置磺陡,代碼如下:

復制代碼

/// 構造函數(shù)

SystemConfigModel.init(){

? LocalStorage.get('tyfapp.system.config').then((configStr){

? ? Map<String, dynamic> configInfo = {};

? ? if(configStr == null || configStr == '') { // 判斷合法性

? ? ? configInfo = {

? ? ? ? "accessMessage" : true,

? ? ? ? "tipsDetail" : true,

? ? ? ? "soundReminder" : true,

? ? ? ? "vibrationReminder" : true

? ? ? };

? ? } else {

? ? ? try { // 嘗試 json 解析,解析失敗直接返回

? ? ? ? configInfo =

? ? ? ? json.decode(configStr) as Map<String, dynamic>;

? ? ? } catch(e){

? ? ? ? return;

? ? ? }

? ? }

? ? systemConfig = StructSystemConfig.fromJson(configInfo);

? });

}

上面代碼 init 為構造函數(shù)漠畜,其中第 3 行是異步讀取文件币他,獲取到文件后存儲在共享狀態(tài)變量 systemConfig 中。為了異炽灸考慮蝴悉,如果沒有獲取到文件內容,則將共享狀態(tài)變量默認設置打開狀態(tài)瘾敢,也就是 true 值拍冠。有了初始化部分尿这,再修改 main.dart 增加一個新的狀態(tài)共享,部分如下代碼:

復制代碼

// 初始化共享狀態(tài)對象

LikeNumModel likeNumModel = LikeNumModel();

NewMessageModel newMessageNum = NewMessageModel(newMessageNum: 0);

// 異步數(shù)據(jù)處理

ApiUserInfoMessage.getUnReadMessageNum(newMessageNum);

// 異步獲取系統(tǒng)配置

SystemConfigModel systemConfigModel = SystemConfigModel.init();

return MultiProvider(

? providers: [

? ? ChangeNotifierProvider(create: (context) => likeNumModel),

? ? ChangeNotifierProvider(

? ? ? ? create: (context) => UserInfoModel(myUserInfo: myUserInfo)),

? ? ChangeNotifierProvider(create: (context) => newMessageNum),

? ? ChangeNotifierProvider(create: (context) => systemConfigModel),

? ],

? child: child,

);

上面代碼的第 7 行就是增加了系統(tǒng)變量的初始化庆杜,第 15 行就是增加到狀態(tài)共享中射众。接下來我們完善下 system_config_model.dart 代碼,為其增加 get 和 save 方法欣福,代碼如下:

復制代碼

/// 轉化為StructSystemConfig結構

StructSystemConfig get() {

? return systemConfig;

}

/// 轉化為StructSystemConfig結構

bool getSwitchItem(String switchItem) {

? if(systemConfig == null) {

? ? return false;

? }

? Map<String,dynamic> systemConfigJson =

? StructSystemConfig.toJson(systemConfig);

? try{

? ? return systemConfigJson[switchItem] as bool;

? }catch(e){

? ? return false;

? }

}

代碼的第 2 到第 18 行中的兩個方法 get 和 getSwitchItem 责球,其作用都是獲取系統(tǒng)配置,前者是獲取所有配置拓劝,后者是獲取具體的某個配置雏逾。我們繼續(xù)看下配置保存的兩個方法,代碼如下郑临。

復制代碼

/// 保存單個數(shù)據(jù)

void saveOne(String key, bool value) {

? Map<String,dynamic> systemConfigJson =

? ? StructSystemConfig.toJson(systemConfig);

? if(systemConfigJson[key] == value) {

? ? return;

? }

? systemConfigJson[key] = value;

? systemConfig = StructSystemConfig.fromJson(systemConfigJson);

? print(systemConfigJson);

? LocalStorage.save(json.encode(systemConfigJson), 'tyfapp.system.config');

? notifyListeners();

}

/// 整體數(shù)據(jù)保存

void save(StructSystemConfig newSystemConfig) {

? if(

? systemConfig.accessMessage == newSystemConfig.accessMessage &&

? ? ? systemConfig.tipsDetail == newSystemConfig.tipsDetail &&

? ? ? systemConfig.soundReminder == newSystemConfig.soundReminder &&

? ? ? systemConfig.vibrationReminder == newSystemConfig.vibrationReminder

? ) {

? ? return;

? }

? systemConfig = newSystemConfig;

? LocalStorage.save(

? ? ? json.encode(StructSystemConfig.toJson(systemConfig)),

? ? ? 'tyfapp.system.config'

? );

? notifyListeners(

代碼 save 和 saveOne栖博,分別對應保存整個系統(tǒng)配置數(shù)據(jù)和保存單個系統(tǒng)配置數(shù)據(jù)。在兩者實現(xiàn)邏輯中厢洞,首先都做了前期數(shù)據(jù)校驗判斷仇让,避免不必要的 build 操作。在 save 代碼邏輯中躺翻,需要將數(shù)據(jù)存儲到本地丧叽,通過調用 LocaStorage.save 來實現(xiàn)。

組件應用

組件應用部分較為簡單公你,我們先看下 pages/system_page/index.dart 的邏輯踊淳,如下:

復制代碼

import 'package:flutter/material.dart';

import 'package:provider/provider.dart';

import 'package:two_you_friend/model/system_config_model.dart';

import 'package:two_you_friend/widgets/system_page/switch_card.dart';

import 'package:two_you_friend/util/struct/system_config.dart';

/// 首頁

class SystemConfigPageIndex extends StatelessWidget {

? /// 構造函數(shù)

? const SystemConfigPageIndex();

? @override

? Widget build(BuildContext context) {

? ? final systemConfigModel = Provider.of<SystemConfigModel>(context);

? ? StructSystemConfig systemConfig = systemConfigModel.get();

? ? return Container(

? ? ? padding: EdgeInsets.all(8),

? ? ? child: Column(

? ? ? ? children: <Widget>[

? ? ? ? ? SystemPageSwitchCard(itemDesc: '新消息提醒', switchItem: 'accessMessage'),

? ? ? ? ? SystemPageSwitchCard(itemDesc: '通知顯示詳情', switchItem: 'tipsDetail'),

? ? ? ? ? SystemPageSwitchCard(itemDesc: '聲音', switchItem: 'soundReminder'),

? ? ? ? ? SystemPageSwitchCard(itemDesc: '振動', switchItem: 'vibrationReminder')

? ? ? ? ],

? ? ? ),

? ? );

? }

}

主要邏輯在 build 中,build 中使用了 widgets/system_page/switch_card.dart 陕靠,我們看下這個子組件的實現(xiàn)迂尝,代碼如下:

復制代碼

import 'package:flutter/material.dart';

import 'package:provider/provider.dart';

import 'package:two_you_friend/model/system_config_model.dart';

import 'package:two_you_friend/styles/text_syles.dart';

/// 單個系統(tǒng)配置

///

/// [title]為帖子詳情內容

class SystemPageSwitchCard extends StatelessWidget {

? /// 傳入的帖子標題

? final String switchItem;

? /// 消息提醒文字

? final String itemDesc;

? /// 構造函數(shù)

? const SystemPageSwitchCard(

? ? ? {Key key, this.itemDesc, this.switchItem}

? ? ? ) : super(key: key);

? @override

? Widget build(BuildContext context) {

? ? // 獲取操作句柄

? ? final systemConfigModel = Provider.of<SystemConfigModel>(context);

? ? return Row(

? ? ? mainAxisAlignment: MainAxisAlignment.spaceBetween,

? ? ? children: <Widget>[

? ? ? ? Text(

? ? ? ? ? itemDesc,

? ? ? ? ? style: TextStyles.commonStyle(1, Colors.black),

? ? ? ? ),

? ? ? ? Switch( // 選擇

? ? ? ? ? ? value: systemConfigModel.getSwitchItem(switchItem),

? ? ? ? ? ? activeTrackColor: Colors.lightBlueAccent,

? ? ? ? ? ? materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,

? ? ? ? ? ? onChanged: (newValue) { // 觸發(fā)狀態(tài)變化

? ? ? ? ? ? ? systemConfigModel.saveOne(

? ? ? ? ? ? ? ? ? switchItem,

? ? ? ? ? ? ? ? ? !systemConfigModel.getSwitchItem(switchItem)

? ? ? ? ? ? ? );

? ? ? ? ? ? }

? ? ? ? ),

? ? ? ],

? ? );

? }

}

代碼第 34 行使用了 Switch 這個組件,該組件的 value 是通過 systemConfigModel 狀態(tài)共享類獲取剪芥,在點擊切換時觸發(fā)狀態(tài)改變垄开,并調用 systemConfigModel 中的 saveOne 觸發(fā)依賴組件狀態(tài)變化。

以上就實現(xiàn)了系統(tǒng)設置的功能税肪,相對其他組件的實現(xiàn)溉躲,這部分邏輯較為復雜,涉及了 Flutter 的本地存儲 以及 Provider 的應用技術點益兄。

搜索

最后我們來看下搜索功能签财,前面我們已經(jīng)實現(xiàn)了一個基本的搜索功能,但是其中的接口部分沒有補齊偏塞,我們先來看下實際的效果,如圖 6 所示邦鲫。

圖 6 搜索提示和搜索結果效果

組件樹+布局

根據(jù)圖 6 的頁面效果灸叼,我們來繪制組件樹+布局神汹,搜索提示就是一個列表,這里就不繪制了古今,搜索結果稍微復雜一些屁魏,主要看下這部分,繪制結果如圖 7 所示捉腥。

圖 7 搜索結果頁面組件樹+布局設計

這個組件樹的設計氓拼,包含了我們布局設計思想中的 8 個過程,豎橫抵碟、高寬桃漾、上下和左右,具體細節(jié)就不再贅述拟逮。接下來我們看下這兩部分邏輯的實現(xiàn):搜索提示和搜索結果撬统。

搜索提示

搜索提示較為簡單,主要邏輯是從服務端拉取搜索提示接口敦迄,并返回一個 ListView 列表結果恋追。具體代碼如下:

復制代碼

/// 獲取 suggest list組件列表

Future<Widget> _getSuggestList() async{

? List<String> suggests = await ApiSearchIndex.suggest(query);

? if(suggests == null || suggests.length < 1){ // 異常處理

? ? return Container();

? }

? // 保留前 5 個搜索

? int subLen = suggests.length > 5 ? 5 : suggests.length;

? List<String> subSuggests = suggests.sublist(0, subLen);

? return ListView.builder(

? ? ? scrollDirection: Axis.vertical,

? ? ? shrinkWrap: true,

? ? ? itemCount: subSuggests.length,

? ? ? itemBuilder: (context,index){

? ? ? ? return? ListTile(

? ? ? ? ? ? title: RichText(

? ? ? ? ? ? ? ? text: TextSpan(

? ? ? ? ? ? ? ? ? // 獲取搜索框內輸入的字符串,設置它的顏色并加粗

? ? ? ? ? ? ? ? ? ? text: subSuggests[index],

? ? ? ? ? ? ? ? ? ? style: TextStyles.commonStyle()

? ? ? ? ? ? ? ? )

? ? ? ? ? ? ),

? ? ? ? ? ? onTap: () {

? ? ? ? ? ? ? query = subSuggests[index];

? ? ? ? ? ? ? showResults(context);

? ? ? ? ? ? },

? ? ? ? );

? ? ? }

? );

}

代碼中罚屋,首先使用 query 關鍵詞獲取用戶輸入苦囱,通過 ApiSearchIndex.suggest 方法獲取服務端搜索提示結果,接下來做一些數(shù)據(jù)校驗脾猛,最后根據(jù)搜索提示 build 出相應的組件撕彤。其中的第 26 行至第 28 行代碼的作用是,通過點擊搜索提示觸發(fā)搜索行為尖滚,第 27 行替換搜索提示內容喉刘,第 28 行執(zhí)行搜索并獲取搜索結果。

搜索結果

根據(jù)圖 7 的繪制結果漆弄,我們了解到這里需要設計兩個組件睦裳,組件一是展示搜索到的用戶列表內容,組件二是展示搜索到的帖子列表內容撼唾。我們這里就使用兩個組件函數(shù)來實現(xiàn)廉邑,主要看下用戶部分(帖子部分邏輯相似)。

復制代碼

/// 獲取用戶搜索結果組件

Widget _getUserListWidget(List<StructUserInfo> userList) {

? if(userList == null || userList.length < 1){

? ? return Container();

? }

? int subLen = userList.length > 5 ? 5 : userList.length;

? List<StructUserInfo> subUserList = userList.sublist(0, subLen);

? return ListView.builder(

? ? ? scrollDirection: Axis.vertical,

? ? ? shrinkWrap: true,

? ? ? itemCount: subUserList.length + 1,

? ? ? itemBuilder: (context,index) {

? ? ? ? if(index == 0){

? ? ? ? ? return Row(

? ? ? ? ? ? children: <Widget>[

? ? ? ? ? ? ? Padding(padding: EdgeInsets.only(left: 10)),

? ? ? ? ? ? ? Text(

? ? ? ? ? ? ? ? '用戶',

? ? ? ? ? ? ? ? style: TextStyles.commonStyle(0.9),

? ? ? ? ? ? ? ),

? ? ? ? ? ? ],

? ? ? ? ? );

? ? ? ? }

? ? ? ? return UserPageCard(userInfo: subUserList[index - 1]);

? ? ? });

}

以上組件代碼的實現(xiàn)與我們之前所學習的知識點倒谷,沒有太大的差異性蛛蒙。核心知識點是應用 ListView.builder 組件,來顯示 seaction_name (也就是上面的 Text 組件)和搜索結果中的用戶列表(上面的 UserPageCard 組件)渤愁。

以上就完成了搜索部分的邏輯牵祟,具體代碼查看 github 中的 pages/search_page/custom_delegate.dart 文件。

總結

本課時帶領大家實踐開發(fā)了四個核心頁面(我的好友抖格、我的消息诺苹、系統(tǒng)設置和搜索)咕晋。學完本課時你需要進一步掌握組件樹+布局的設計思想,同時掌握 Flutter 本地存儲的技術點收奔,進一步鞏固 Flutter 編碼風格掌呜。學完本課時之后,我建議你自行去實現(xiàn)“我的消息”中的私信功能和評論相關的部分(后續(xù)會在 github 上提供源碼)坪哄。

本課時之前质蕉,我們對 App 的安全并沒有關注太多,可以說完全放任翩肌。下一課時我們將通過工具化的方式來上報異常模暗,保證我們 App 的質量,提前發(fā)現(xiàn)并解決問題摧阅。

轉自:https://kaiwu.lagou.com/course/courseInfo.htm?courseId=251#/detail/pc?id=3535

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末汰蓉,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子棒卷,更是在濱河造成了極大的恐慌顾孽,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,509評論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件比规,死亡現(xiàn)場離奇詭異若厚,居然都是意外死亡,警方通過查閱死者的電腦和手機蜒什,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,806評論 3 394
  • 文/潘曉璐 我一進店門测秸,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人灾常,你說我怎么就攤上這事霎冯。” “怎么了钞瀑?”我有些...
    開封第一講書人閱讀 163,875評論 0 354
  • 文/不壞的土叔 我叫張陵沈撞,是天一觀的道長。 經(jīng)常有香客問我雕什,道長缠俺,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,441評論 1 293
  • 正文 為了忘掉前任贷岸,我火速辦了婚禮壹士,結果婚禮上,老公的妹妹穿的比我還像新娘偿警。我一直安慰自己躏救,他們只是感情好,可當我...
    茶點故事閱讀 67,488評論 6 392
  • 文/花漫 我一把揭開白布螟蒸。 她就那樣靜靜地躺著盒使,像睡著了一般睁本。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上忠怖,一...
    開封第一講書人閱讀 51,365評論 1 302
  • 那天,我揣著相機與錄音抄瑟,去河邊找鬼凡泣。 笑死,一個胖子當著我的面吹牛皮假,可吹牛的內容都是我干的鞋拟。 我是一名探鬼主播,決...
    沈念sama閱讀 40,190評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼惹资,長吁一口氣:“原來是場噩夢啊……” “哼贺纲!你這毒婦竟也來了?” 一聲冷哼從身側響起褪测,我...
    開封第一講書人閱讀 39,062評論 0 276
  • 序言:老撾萬榮一對情侶失蹤猴誊,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后侮措,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體懈叹,經(jīng)...
    沈念sama閱讀 45,500評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,706評論 3 335
  • 正文 我和宋清朗相戀三年分扎,在試婚紗的時候發(fā)現(xiàn)自己被綠了澄成。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,834評論 1 347
  • 序言:一個原本活蹦亂跳的男人離奇死亡畏吓,死狀恐怖墨状,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情菲饼,我是刑警寧澤肾砂,帶...
    沈念sama閱讀 35,559評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站巴粪,受9級特大地震影響通今,放射性物質發(fā)生泄漏。R本人自食惡果不足惜肛根,卻給世界環(huán)境...
    茶點故事閱讀 41,167評論 3 328
  • 文/蒙蒙 一辫塌、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧派哲,春花似錦臼氨、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,779評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽感耙。三九已至,卻和暖如春持隧,著一層夾襖步出監(jiān)牢的瞬間即硼,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,912評論 1 269
  • 我被黑心中介騙來泰國打工屡拨, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留只酥,地道東北人。 一個月前我還...
    沈念sama閱讀 47,958評論 2 370
  • 正文 我出身青樓呀狼,卻偏偏與公主長得像裂允,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子哥艇,可洞房花燭夜當晚...
    茶點故事閱讀 44,779評論 2 354