PartyBid 學習筆記 之 第二張卡片總結

此博客已棄,請轉至 此處

本文僅爲培訓期間應試做文,不具任何教學價值,具體問題請參考對應文章。javascript

AngularJS LOGO


前情提要

Party Bid 是一款基於 AngularJS 的安卓網頁應用。所謂安卓網頁應用,指的是應用徹底使用網頁開發模式構造(HTML + CSS + JavaScript),以後使用 Apache Cordova 工具將其生成爲安卓本地應用項目。css

對於應用內容的介紹,考慮到本文的面向讀者,此處再也不詳細說明,主要內容在於 開發過程當中所用到的技術我的學習的一些心得體會html

在第二張卡片中,增長了活動報名的一些功能,主要涉及內容能夠參考下列文章:java


數據結構設計

在卡片一中,爲了保證良好的可讀性和可拓展性,採用了 對象數組 的形式來在 locolStorage 中存儲活動。爲了可以實現對活動報名相關內容的存儲,咱們須要對數據結構進行相應的拓展。git

在活動報名中,主要增長了三個方面的信息: 活動的報名狀態當前正在進行的活動人員的報名信息程序員

在數據庫設計中,應當遵循一點,一個數據表中的每一個數據項中不該當存在數組類型字段。對於有嵌套關係的不定項內容,應該增長子表來拓展。但對於 Json 字符串來講,因爲其主要用於數據傳遞時,爲了提升效率,能夠具備不定項的嵌套存在,讀者應該對數據結構的使用有必定基本瞭解。github

1.活動的報名狀態

對於活動的報名狀態,咱們在 Activity 類中增長一個 register 字段,主要代碼以下:web

function Activity(name, createdAt, register) {
    this.name = name;
    this.createdAt = createdAt || Date.parse(new Date());
    this.register = register || 'prepare';
}

注意:讀者如今應當理解 Activity 類activity.js 和特定語境下的 model 指的是同一個東西。正則表達式

register 字段具備 3 個枚舉值:preparerunover 。分別表示活動的 未開始進行中已結束算法

使用邏輯或能夠方便地設定默認值,此處設爲 prepare 狀態。

2.當前正在進行的活動

因爲短信的接收和處理在後臺進行,所以並不在一個特定的頁面,沒法使用 url 中的路由參數或 $scope 中的變量來肯定當前的活動。爲此,咱們須要一個能在全局肯定當前活動的方法(還真的是方法 0.0)。

在 Activity 類中添加以下代碼:

Activity.now = function () {
    var activity_now = localStorage.getItem("now") || "";
    return Activity.find_by_name(activity_now);
};

其中, localStorage 的 now 字段用於存儲 當前或最近進行活動的名稱 , 接着經過調用 Activity 類的 find_by_name 方法來轉換爲對象實例。

固然,咱們須要定義 Activity 類的 find_by_name 方法:

Activity.find_by_name = function (activity_name) {
    var found = _(Activity.all()).findWhere({name: activity_name}) || {};
    return new Activity(found.name, found.createdAt, found.register);
};

等一下,上面的代碼中好像出現了奇怪的符號和奇怪的方法?!

其實這個符號也並不奇怪,下劃線 _ 是 JavaScript 中合法的標識符,在這裏,它是咱們所使用的 UnderScoreJS 的類名,而 findWhere 是其一個函數,功能爲返回指定集合中 知足給定鍵值對 的第一個元素。(也能夠用於對象,用法略有不一樣)

關於 UnderScoreJS 的詳細介紹能夠參考 UnderscoreJS 之 消滅for循環

3.人員的報名信息

人員的報名信息具備本身獨立內容且與活動相關,爲此增長一個新的 model —— Register.js

以後,繼續添加 構造函數讀取函數存儲函數

構造函數以下:

function Register(name, phone, activity, createdAt) {
  this.name = name;
  this.phone = phone;
  this.activity = activity || Activity.now().name;
  this.createdAt = createdAt || Date.parse(new Date());
}

在一條報名信息中,具備 人員姓名人員電話號碼所屬活動建立時間 4 個字段。

以後,咱們也須要像 Activity 類那樣添加類的 all 方法和實例的 save 方法,具體步驟基本相同,只是把 localStorage 的 activities 字段換成 registers 便可,此處不給出具體代碼。


獲取當前頁面的活動

嚴格的來講,這依然是卡片一的需求,可是卡片一中並無直接使用。

在卡片一中,咱們已經設置了好了路由,並將活動名稱做爲路由參數傳遞。

注意:在實際應用中,名稱並非做爲路由參數一個很好的選擇,考慮到需求中已經說明了名稱不能重複,故其具備惟一性,能夠做爲數據表的主項。不然,應當增長一個不重複的 id 字段來做爲惟一標識。

在上面的代碼中,咱們已經定義了 Acitvity.find_by_name 方法,如今,咱們須要經過它把當前頁面的活動讀取到 controller 當中:

將 ActivityRegisterCtrl 中的初始化代碼改成:

...
$scope.this_activity = Activity.find_by_name($routeParams.activityName);
...

這樣, $scope.this_activity 就再也不是一個活動名稱,而是一個 Activity 實例的引用。

接着就可以獲取其 報名狀態

...
$scope.status = this_activity.register;
...

至此, 報名狀態 就成爲了 $scope 的一個屬性,之因此單獨提出是爲了方便數據綁定,也能夠直接以 this_activity.register 來使用。


報名頁面的神奇按鈕

在卡片二中,存在以下需求:

點擊「活動報名」頁面的「開始」按鈕,活動報名開始,頁面中的「開始」按鈕替換爲「結束」按鈕,報名開始。

【結束】按鈕變爲【開始】按鈕,若是點擊【開始】按鈕,則能夠繼續以前的報名。

對於按鈕的 開始 / 結束 切換,這裏介紹 3 種方法。

1.直接內容綁定

最簡單的一種方法,也是最容易實現的,可是代碼略多。

在 view 中,將 button 的內容設爲 AngularJS 的 ng-bind :

<header>
  ...
  <button class='sth' ng-click='sth'>{ { button_content } }</button>
  ...
</header>

考慮到如今使用的事 jade 模板,其代碼應爲:

header
  ...
  button.sth(ng-click='sth') { { button_content } }
  ...

在 jade 語言中,沒有尖括號,不須要封閉,使用縮進表示嵌套關係,使用 . 表示 class ,屬性放在括號中。

關於 jade 的詳細語法介紹,請參見:Jade —— 簡潔的HTML模版引擎

對於 ng-click 的運做,將在下一小節中實現。

同時,在 controller 中對其進行賦值:

...
$scope.button_content = status == 'run'? '結束': '開始';
...

讀者要特別注意邏輯關係,若是正在進行中,應該是顯示 結束 ;反之,若是沒在進行,纔是顯示 開始

2.使用 ng-switch 切換

ng-switch 用於根據條件決定顯示項,功能和 ng-if 類似,至關於 ng-if + ng-else(實際不存在)。

這裏的基本過程以下:

  1. 對 header 控件添加 ng-switch on 屬性。
  2. 建立兩個按鈕控件,添加相反的 ng-switch-when 屬性。
  3. ng-switch on 的值改變時,顯示的變爲隱藏,隱藏的變爲顯示。

所以,在這裏 不推薦 使用 ng-switch ,理由以下:

  1. ng-switch 建立了 2 個按鈕,增長了沒必要要的 view 層代碼。
  2. 這裏根本不符合 ng-switch 的使用理念,其目的是爲了根據條件顯示不一樣的控件組,好比在一個問卷(HTML 表單)中,根據前面的性別選項決定後面須要出現的問題組。而在此處,自己就確實是一個按鈕,只是由於一條屬性的改變被拆分紅 2 個按鈕,這樣在算法設計上就已是一種倒退。
  3. 可能存在靜態冒險(學多了數電,就這樣叫吧 >o<),便可能在一個瞬間 2 個按鈕同時顯示或同時不顯示。

綜上所述,並不推薦使用這種方法。

如下給出的實例代碼僅供參考,讓讀者瞭解 ng-switch 的用法:

view 中(使用 jade 實現,下同):

header(ng-switch on="status")
  ...
  button.sth(ng-click='sth' ng-switch-when='status=="run"') 結束
  button.sth(ng-click='sth' ng-switch-when='status!="run"') 開始
  ...

在 ng-switch 中,還可使用 ng-switch-default 屬性指定在沒有 ng-switch-when 知足時顯示某(些)控件。

3.傳說中的美顏濾鏡

AngularJS 中, filter 是一個重要功能,確切的說,是一套重要體系。

在不少文章中, filter 被翻譯成過濾器,這樣在很大程度上容易形成沒必要要的誤會,以爲 filter 是用來進行數據篩選的。(固然也確實能夠)

英文的語言風格和中文具備很大的不一樣,當出現一個新興事物時,中文每每會增長一個詞彙;而英文因爲其僅僅只有區區 26 個字母,排列組合有限,而且每一個單詞必須知足特定字母順序和結構(全是輔音看你怎麼讀 >.<),因此英文每每只是在一個已有單詞中增長一個 義項 (不過理由是我瞎扯的 T.T),好比 mouse 在平常生活中指的不必定是老鼠也能夠是鼠標,這樣的例子還有不少(因此好好學英語吧 ^.^)。

filter 還有一個在生活中(Both 攝影 and 後期)更爲經常使用的義項 —— 濾鏡

在攝影中,有不少種濾鏡,好比 偏振鏡、UV 鏡、漸變鏡、移軸鏡... What!買不起單反?Photoshop 中的濾鏡總該用過吧... Nany?不會修圖?誰敢說本身真的不會修圖?沒自拍過麼?沒用過美圖秀秀麼?一鍵美化也是用了濾鏡的啊!

好了,迴歸主題,總之在這裏譯成 濾鏡 是更爲合適的,其用途在於對原始數據的 模式轉換 。考慮到大部分程序員都不是英語專業也不是攝影專業的,對 filter 的第一感受可能都是過濾器(其實我也是這麼感受的,雖然知道,就和看到 mouse 就自動譯成老鼠同樣),但但願讀者瞭解,這裏的 filter ,和諸如 fomatter , parser , encoder , decoder , convertor 是一個意思。什麼?一個都不認識?那仍是先學英語吧...

舉個最簡單的例子,在大部分國家或者地區,性別都是能夠用一個 boolean 變量(raw)來存儲的,可是須要顯示的時候,就須要轉換成對應的字符串,或者是丘比特的弓箭和維納斯的鏡子(pretty)。這裏就是一個增長冗餘數據的過程,通常來講也都是美化過程,故應將其譯做濾鏡而非過濾器。

爲了使用自定義的濾鏡,咱們須要在 module 中配置該濾鏡,這裏將其命名爲 switch(開關)。

_在 app/scripts 文件夾下,新增一個 filter 文件夾,並在其中添加一個 switch.js 的文件。_你是這麼想的麼?我一開始也是這麼想的。可是由於有 Yo 這個神器的存在,這種體力活徹底不須要咱們本身來作。

打開終端,進入到 party_bid 文件夾。

注意:實際上,在大多數狀況下,均可以直接在特定文件夾打開終端。好比在 Mac 中,直接將 Finder 頂部的文件夾圖標拖到 Dock 上的終端裏;或者在 Sublime Text 3 中安裝一個 Terminal 插件,就能直接在目錄樹裏打開當前路徑的終端。

終端命令以下:

$ yo angular:filter switch

這樣,就已經自動建立好了文件和基本代碼了,何樂而不爲呢?

考慮到咱們要將活動的報名狀態($scope.status)轉換爲按鈕的顯示內容,修改代碼以下:

view 中:

header
  ...
  button.sth(ng-click='sth') { { status | switch } }
  ...

filter 中:

angular.module('partyBidApp')
  .filter('switch', function () {
    return function (input) {
      return input == 'run'? '結束': '開始';
    };
  });

這樣,就可以自動將報名狀態轉換爲按鈕的顯示文字了。

雖然 filter 能實現的功能均可以直接經過 controller 實現,但其可以充分實現 代碼複用 的理念,對於大型項目來講是不可缺乏的。


活動的開始與結束

在卡片二中,存在以下需求:

點擊「活動報名」頁面的「開始」按鈕,活動報名開始,頁面中的「開始」按鈕替換爲「結束」按鈕,報名開始。

報名開始後,組織者誤點擊「結束」按鈕。彈出一個「報名結束確認」提示,二次詢問是否要結束報名。

【結束】按鈕變爲【開始】按鈕,若是點擊【開始】按鈕,則能夠繼續以前的報名。

只要有活動在報名,其餘活動「活動報名」頁面上的「開始」按鈕就爲不可點擊的灰色狀態。

在上一小節中,咱們已經實現了按鈕的顯示,但尚未對其添加任何功能。

爲此,咱們須要爲 button 添加 ng-click 屬性,上一小節中僅僅是爲了說明 jade 語法而放置了一個內容佔位符。

1.開始或結束活動

在 view , controller 和 model 中,添加代碼以下:

view 中:

header
  ...
  button.sth(ng-click='turn_status()') { { status | switch } }
  ...

這裏假定上一小節中讀者使用了 filter 實現。

controller 中:

...
$scope.turn_status = function () {
  if($scope.status != 'run' || window.confirm('確認要結束本次報名嗎?!'')) {
    $scope.this_activity.turn_register();
    $scope.initiate_data();
  }
};
...

這裏用到了一點邏輯思惟,"若是活動報名沒在進行或者彈窗獲得確定答案,那麼執行本頁活動的改變狀態函數?!"。

看起來有點繞,拆開來就是:

  • 若是是要開始活動,跳過彈窗直接執行;
  • 若是是要結束活動,彈窗提示,獲得確認才執行,不然不執行;
  • 執行完畢後從新初始化當前數據。

最後一條也能夠經過直接改變 status 實現,但存在冒險行爲,即萬一 model 中的函數執行發生了某種意外,就可能致使頁面顯示的狀態和後臺存儲的狀態不一樣。

固然,在真實的 Web App 中,異步更新數據是一個更好的選擇。由於數據存儲在服務器端,數據操做須要很大的延遲,這樣作更利於知足用戶體驗,即直接改變當前頁面的狀態,同時後臺更新服務器數據,假如獲得服務器數據返回的失敗提示後,再在本地作出相應的處理。

此處考慮到是安卓的本地應用,不存在數據操做的可測延遲,故能夠同步更新數據。但願讀者在實際案例中自行比較各類方式的利弊而後作出決定,而不是一味地套用以前的代碼。

model 中:

Activity.prototype.turn_register = function () {
  var next_status = (this.register == 'run'? 'over': 'run');
  Activity.alter_status(this.name, next_status);
};

這裏的整個過程當中,活動的開始和結束 共用同一個函數 ,爲此須要判斷活動狀態。

接着,調用 Activity 的 alter_status 方法:

Activity.alter_status = function (the_name, status) {
  var list = Activity.all();
  var found = _(activity_list).findWhere({name: the_name});
  found.register = status;
  localStorage.setItem('activities', JSON.stringify(list));
  Activity.update_now(found);  
};

這裏再次調用了 UnderScoreJS 的 findWhere 方法。以後又調用了 Activity 的 update_now 方法:

Activity.update_now = function (activity) {
  localStorage.setItem('now', activity.name);
};

該函數用於改變當前活動的標記。

在上面的代碼中,3 個函數加起來都不到 15 行。可是爲了保證可讀性和可維護性,應當使得每一個函數 只實現單一功能

注意:這裏要求任何數據操做必須使用實例函數,不然直接使用類函數的話確實能夠更加簡單一點的。

2.有活動進行中開始按鈕不可用

爲了實現這個操做,咱們仍是須要在 view , controller 和 model 中添加相應代碼:

view 中:

header
  ...
  button.sth(ng-click='turn_status()',ng-disabled='no_start') { { status | switch } }
  ...

上面在 button 控件中添加了一個 ng-disabled 屬性,注意 jade 中屬性之間可使用 逗號 隔開(也能夠空格)。

controller 中:

...
$scope.no_start = $scope.status != 'run' && Activity.on_going();
...

上面代碼中,先判斷當前是否活動未在進行(即按鈕顯示爲"開始"),接着在調用 Activity.on_going 方法判斷是否有活動在進行。

接着定義 Activity.on_going 方法的實現。

model 中:

Activity.on_going = function () {
    return _(Activity.all()).some(function (activity) { 
        return activity.register == "run";});
};

上面使用到了 UnderScoreJS 的 some 方法,其用途爲在給定集合中是否存在知足條件(即返回值爲 Truthy )的元素。

至此,就可以根據狀況實現"開始"的不可用,和建立活動時"返回"按鈕的不可見的方法相似。


進行中的列表項變黃

在卡片二中,存在以下需求:

點擊【返回】按鈕,返回「活動列表」頁面,活動列表中正在報名中的活動底色爲黃色。

要實現的內容就是在 ng-repeat 中對控件的樣式進行動態綁定。

這裏依然給出 3 中方法。

1.直接使用 ng-bind 綁定 class 屬性
...
ul.sth
  li(ng-repeat='activity in activitys | orderBy: ...')
    a(class='sth { {someColor(activity.register)} }',ng-click='..') ..
...

即把顏色屬性寫成 css 而後以活動報名狀態爲參數經過調用 $scope.someColor 方法返回相應的 css 字段。

controller 中的代碼過於簡單,此處從略。

2.使用 ng-class 指定屬性
...
ul.sth
  li(ng-repeat='activity in activitys | orderBy: ...')
    a(ng-class='{yelloCss: status=="run", whiteCss: status!="run"}',ng-click='..') ..
...

其中,yelloClasswhiteClass 爲對應的 css 屬性名。

這裏是 ng-class 的一種用法,ng-class 總共具備 3 種用法:

  1. 直接給定 字符串(可含空格)變量,和上面 class 用法類似,只是不須要花括號。
  2. 給定 數組 變量,其中每一個元素均爲可含空格的字符串,所有做爲 class 屬性。
  3. 給定 對象 ,對於每一個鍵值對,以全部值爲真的鍵名做爲 class 屬性。

本處使用的是第 3 中方法,也是最複雜可是最實用的方法。

3.繼續使用濾鏡

將 view 中代碼改成:

...
a(ng-class='activity.register | yello',ng-click='..') ..
...

建立一個很黃很暴力的濾鏡:

$ yo angular:filter yello

其核心代碼以下:

...
return input == 'run'? 'yelloCss': 'whiteCss';
...

雖說在第 2 種方法的 ng-class 中學到了新東西,但站在工程角度來講,對於這類具備明確的 一一映射 屬性的變量仍是推薦使用濾鏡來實現。

在具備更爲複雜的動態 css 系統時,ng-class 將會是一種很好的選擇。


短信的後臺處理

在卡片二中,存在以下需求:

報名開始後,報名者發送短信:BM+姓名 到18601126251進行報名後,報名者接收到一條由系統返回的報名確認信息,「恭喜!報名成功」。

BM的大小寫不限,BM後能夠有空格。

報名者重名,若是來自不一樣的手機號碼,保留重名者。

若是報名者在活動建立完,可是第一次點擊活動按鈕前,開始前發送報名短信,系統返回其一條錯誤信息,「活動還沒有開始,請稍後」。

報名者在活動報名結束後發送信息報名,系統返回一條錯誤信息,「Sorry,活動報名已結束」。

在沒有部署到安卓設備上以前,咱們使用 瀏覽器控制檯 來模擬短信收發。

再次新建一個 model —— message.js

在 sms.js 中添加以下代碼:

...
process_received_message: function (json_message) {
  Message.receive(json_message);
...

即將收到短信後的 全部操做 都在 Message 類中進行,sms.js 僅做爲庫文件。

1.校驗短信類型

對於一個手機號碼,其可能收到任何短信,包括朋友聊天、垃圾廣告、新聞推送、驗證碼接收等等。而咱們目前僅僅須要接收報名短信。

Message.received_new_item = function (message_json) {
  var text = message_json.messages[0].message;
  var phone = message_json.messages[0].phone;
  var header = text.substring(0,2).toUpperCase();
  if(header == "BM") {
    Message.cope_new_register(Message.get_name(text), phone);
  }
};

上面對短信內容進行判斷,確認其是否爲 BM , Bm , bM 或 bm 開頭。爲此,直接將其 轉爲大寫 與 BM 比較。

Message.get_name 用來去除可能的空格:

Message.get_name = function (text) {
  return text.substring(2).replace(/\s/g, '');
};

replace 中,使用了正則表達式進行匹配。在 javascript 中,/ / 之間的內容會被識別爲正則表達式,其中 \s 表示匹配任何空字符,最後的 g 表示匹配所有(不然只會匹配第一個出現的知足條件部分)。

2.報名有效性驗證

即使是報名短信,也要在活動進行中才有效,而且須要防止同一人屢次報名,處理代碼以下:

Message.cope_new_register = function (name, phone) {
  var bad_status = (Activity.now().register != "run");
  if (!bad_status && (bad_status = Register.check_if_repeat(phone))) {
    status = "repeat";
  }
  Message.sendback_info(phone, status);
  if (!bad_status) {
    var new_register = new Register(name, phone);
    new_register.save();
    Message.refresh_ui_list();
  };
};

上面的代碼中,先判斷活動報名 是否正在進行 ,設立了一個 bad_status 標記,並記錄 status 。若是確實正在進行,再判斷 手機號碼是否重複 ,若是重複,bad_status 也設爲 true ,更新 status ;根據 status 回覆相應內容;若是 bad_status 爲 false ,建立該報名實例並存儲,同時 刷新頁面

經過設立 標記 ,能夠減小 if-else 判斷的使用量, 防止多級縮進

3.回覆報名結果

在上文中,咱們已經對不一樣狀況設立了不一樣的 status ,接下來須要根據具體的 status 回覆相應信息:

Message.sendback_info = function (phone, status) {
  var back_info = {
    'run': '恭喜!報名成功!^o^',
    'prepare': '活動還沒有開始,請稍後~ >.<',
    'over': 'Sorry,活動報名已結束.. =.=',
    'repeat': '您已經報過名了,請勿浪費短信費.. -_-||'
  };
  var text = back_info[status];
  native_accessor.send_sms(phone, text);
};

這裏,經過創建 哈希表 ,直接經過 索引值 獲得對應的文本數據,避免產生大量的 if-else 語句或者 switch-case 語句。

哈希表是一種很是經常使用而且功能強大的技術,除了做爲字典外,還能夠實現數據存儲、查重等多項任務。


報名頁面的實時刷新

在卡片二中,存在以下需求:

「活動報名」頁面用以列表形式顯示接收到的報名人的姓名和聯繫方式信息並統計報名人數(每一名參與者報名成功後自動更新)。

爲了獲取頁面元素,咱們須要在頁面上添加相應標記,爲此,對活動報名頁面的 view 層添加 id 標籤。

...
#register...
  ...

上面的代碼可能看起來比較隱晦,# 在 jade 中表示 HTML 的 id 屬性,而且 div 標籤能夠 省略不寫

以後,咱們就能經過該 id 來獲取該控件。

Message.refresh_ui_list = function () {
  var ui_scope = angular.element('#register').scope() || { $apply: angular.noop };
  ui_scope.$apply(function (scope) { 
    scope.update_data(); 
  });
};

這裏用到了 angular.element 函數,是否是感受和 jQuery 的 $('#register') 調用方式很像?

其實,angular.element 就是調用了 jQuery 的方法來工做的,若是沒有引用 jQuery ,就會調用 AngularJS 中內置的 jQuery Lite 來實現,返回的也都是 jQuery 對象。

找到元素後,經過 .scope 就能獲取報名頁面 controller 的 $scope,從而能夠發現,能夠在頁面的 任何一個控件 上添加該 id 來實現獲取頁面的 $scope

angular.noop 是一個空函數,什麼都不作。若是當前應用沒有在報名頁面,則獲取到的 $scope 爲 undefined ,所以直接調用其方法會在控制檯報錯(雖然對功能沒有任何影響),做爲一款優秀的應用,須要避免錯誤出現。

以後,咱們須要調用 $scope.update_data 方法。因爲是在 AngularJS 應用外進行調用,咱們須要使用 $scope.$apply 方法,不然 AngularJS 中內置的檢查機制不會獲得響應, controller 中的變化也就沒法傳遞到 view 。controller 中代碼以下:

$scope.update_data = function () {
  $scope.member_list = Register.read_members_of_activity($scope.this_activity);
  $scope.count_of_members = $scope.member_list.length;
};

該函數的做用就是從新加載列表和計算人數,Register.read_members_of_activity 方法可以經過簡單的 UnderScoreJS 函數實現。

Register.read_members_of_activity = function (the_activity) {
  return _.where(Register.all(), {activity: the_activity.name}) || [];
};

在 view 中,使用 濾鏡 處理 count_of_members ,在無人報名時爲 空字符串 ,反之爲 (x)

至此,卡片二中短信處理的核心功能已所有實現。


第二張卡片中主要用的技術和心得體會主要就是這些,若是有任何疑問歡迎在下方回覆 ^.^

本站地址: http://trotyl.github.io/

相關文章
相關標籤/搜索