04-1.play模板標籤詳解

      由於Play採用的是MVC模型,這就要求表現層從請求處理和數據存儲中分離開來。但是靜態頁面的功能畢竟是有限的,不能滿足複雜的顯示的需要,Play對一些常用的模板標籤進行了封裝,不僅實現了表現層和控制層的業務分離,還大大提升了模板功能的重用性。

1. #{a}

      Play需要通過路由器(逆向)生成URL,以此來調用指定的Action。框架封裝了HTML中的超鏈接標籤<a>,可以方便地調用控制器中的Action,如:

#{a @Application.index()} 首頁 #{/a}
      該實例使用了內置標籤#{a},可以生成指向控制器Application中index方法的HTML鏈接元素。在模板執行時,以上代碼會被會解析爲如下HTML代碼:
<a href="/application/index ">首頁</a>

2.#{authenticityToken /}

      使用#{authenticityToken /}標籤可以防止跨站請求僞造,同時也能消除刷新提交和後退提交所帶來的困擾。#{authenticityToken /}首先會爲服務器和客戶端表單生成相同的隨機令牌,並以隱藏的input輸入域的形式嵌入表單。當用戶點擊submit按鈕提交時,令牌會伴隨着表單信息一併發送至控制器中的Action,在Action對提交的表單進行處理前,會先進行令牌的比對,只有令牌信息一致時,才進行後續的表單處理操作。在模板中可以直接使用#{authenticityToken /}標籤:

#{authenticityToken /}

      當爲form表單添加了#{authenticityToken /}標籤後,HTML代碼將會被解析爲:

<input type="hidden" name="authenticityToken"
       
value="1c6d92fed96200347f06b7c5e1a3a28fa258ef7c">

3.#{cache}

      Play的設計理念是無狀態架構,不建議開發人員記錄用戶的狀態。緩存是很好的解決方案,在開發中會被大量的用到,但緩存也有它自身的缺點,因爲緩存只是存在於內存中,適用於存放暫時性的數據,時間一到就會過期。框架對緩存進行了封裝,可以在模板中以標籤的形式對緩存進行操作:

#{cache 'startTime'}
   $
{new java.util.Date()}
#{/cache}
      上例中使用#{cache}標籤,將標籤體內的日期進行緩存。還可以爲#{cache}標籤添加for參數,設置緩存的過期時間:
#{cache 'currentTime', for:'3s'}
   $
{new java.util.Date()}
#{/cache}
      上例爲#{cache}標籤添加了for參數,設置緩存的過期時間爲3秒。

4.#{doLayout /}

      Play引入了模板繼承機制,假設定義裝飾模板main.html,代碼如下:

<!-- common header here -->
<h1>Main template</h1>
 
<div id="content">
    #{doLayout /}
</div>
<!-- common footer here -->
      #{doLayout /}標籤可以包含自身的頁面內容,即表示在標籤處可以插入子模板的內容。

5.#{extends}

      子模板可以通過#{extends}標籤繼承已經定義好的裝飾模板。

#{extends 'main.html'/}

<h1>Some code</h1>
      當子模板使用#{extends}標籤繼承了main.html,就會包含父模板的內容。需要注意的是:在指定模板中別漏掉 #{doLayout /}標籤聲明。

6.#{if},#{ifnot},#{ifError},#{ifErrors}

      在模板中使用#{if}標籤可以進行條件判斷:

#{if user.countryCode == 'en' }
   
Connected user is ${user}
#{/if}

      同時,#{if}標籤還支持複雜的條件判斷:

#{if ( request.actionMethod == 'administer'  && user.isAdmin() ) }
   
You are admin, allowed to administer.
#{/if}

      #{ifnot}標籤顧名思義,就是ifnot的意思,其執行時具體的作用與#{if !condition}等價:

#{ifnot tasks}
   
No tasks today
#{/ifnot}

      可能經常會遇到這種情況,經過控制器中Action的處理,向模板中注入驗證錯誤的提示消息。#{ifError}標籤可以判斷指定的作用域中是否有驗證錯誤的信息:

#{ifError 'user.name'}
 
<p>
   
User name is invalid:
   
#{error 'user.name' /}
 
<p>
#{/ifError}

      只要模板中有任何的驗證錯誤信息,#{ifErrors}標籤都可以將其輸出:

#{ifErrors}
 
<p>Error(s) found!</p>
#{/ifErrors}
7.#{else},#{elseif}

      #{else}標籤通常與#{if}標籤配合使用:

#{if user}
   
Connected user is ${user}
#{/if}
#{else}
   
Please log in
#{/else}
      在Play的模板中,#{else}標籤也可以與#{list}標籤一起使用,當使用#{list }標籤進行迭代的集合爲空時,可以執行#{else}標籤中的內容:
#{list items:task, as:'task'}
   
<li>${task}</li>
#{/list}
 
#{else}
   
Nothing to do...
#{/else}

      #{elseif}標籤的作用同樣是進行條件判斷,用法與#{else}標籤非常類似,也可以與#{list}標籤配合使用:

#{if tasks.size() > 1}
   
Busy tasklist
#{/if}
 
#{elseif tasks}
   
One task on the list
#{/elseif}
 
#{else}
   
Nothing to do
#{/else}

8. #{error},#{errorClass},#{errors}

      #{error}標籤的作用是輸出驗證後的錯誤消息。如果指定字段的錯誤消息確實存在,則直接將其打印出來:

#{error 'user.name'/}

      #{errorClass}標籤的作用是如果存在指定的驗證錯誤消息,則標籤在解析時將被替換爲文本hasError,以此可以用於設置出現錯誤後的HTML元素的樣式。

<input name="name" class="#{errorClass 'name'/}">

      在模板執行時,以上代碼將會按照如下規則解析:

<input name="name" class="${errors.forKey('name') ? 'hasError' : ''}">

      #{errors}標籤可以遍歷當前的驗證錯誤對象集合,其使用方式與 #{list}非常類似,循環體中使用的對象變量名稱是${error}:

<ul>
#{errors}
   
<li>${error}</li>
#{/errors}
</ul>

      #{errors}的標籤體內參數變量有以下幾種:

  • error:表示error對象。
  • error_index:表示當前錯誤在集合中的序號。
  • error_isLast:表示是否是集合中的最後一個錯誤。
  • error_isFirst:表示是否是集合中的第一個錯誤。
  • error_parity:表示當前錯誤在集合中序號的奇偶值,可能是even或odd。
<table>
   
<tr>
       
<th>#</th>
       
<th>Error</th>
   
</tr>
    #{errors}
       
<tr class="${error_parity}">
           
<td>${error_index}</td>
           
<td>${error}</td>
       
</tr>
    #{/errors}
</table>
      由於很多應用中表單的奇偶行的顏色是不同的,使用#{errors}標籤的error_parity參數就可以很方便的解決這個問題,這樣有利於用戶查看錶單,用戶體驗性更強。

9.#{field}

      #{field}標籤的作用非常大,本着Play的“簡約”理念,#{field}標籤可以幫助開發者減少同一變量名的重複使用:

<p>
 
<label>&{'user.name'}</label>
 
<input type="text" id="user_name" name="user.name" value="${user.name}" class="${errors.forKey('user.name') ? 'has_error' : ''}">
 
<span class="error">${errors.forKey('user.name')}</span>
</p>

      以上實例中,表單input輸入中需要大量用到user.name。大量重複操作不僅給開發人員帶來了不便,也使得頁面的代碼顯得非常混亂。使用#{field}標籤很大程度上可以緩解這個問題:

#{field 'user.name'}
<p>
 
<label>&{field.name}</label>
 
<input type="text" id="${field.id}" name="${field.name}" value="${field.value}" class="${field.errorClass}">
 
<span class="error">${field.error}</span>
</p>
#{/}

      Play提倡在頁面開發中儘量使用#{field}標籤,而不是重複地編寫變量名。

10.#{form}

      Play的#{form}標籤封裝了HTML中的<form>元素,在模板中插入#{form}標籤就等價於插入了表單元素。當在模板中使用了#{form}標籤時,Play會從路由配置中自動匹配已經定義好的HTTP方法:如果在路由配置中沒有定義HTTP方法類型,則默認以POST方式請求;如果在路由配置中GET與POST都定義了,則默認以最先定義的HTTP方法執行。#{form}標籤中有以下三種參數:

  • method(可選):定義表單提交的HTTP方法類型,可以是POST或 GET。
  • id(可選):用於定義表單的id屬性。
  • enctype(可選):設置表單數據編碼方式,默認的編碼方式爲application/x-www-form-urlencoded。

注意:

Play中字符編碼方式只能是utf-8。

      在Play模板中使用#{form}標籤的具體示例如下:

#{form @Client.details(), method:'GET', id:'detailsForm'}
   
...
#{/form}

      在模板執行時,以上代碼會被會解析爲如下HTML代碼:

<form action="/client/details" id="detailsForm" method="GET"
     
accept-charset="utf-8">
 ...
</form>

      也可以指定目標實體,作爲Action方法的一部分:

#{form @Client.details(client.id)}
   
...
#{/form}

      假設在控制器中,按照如下定義details方法。該方法中聲明瞭String類型的參數,參數名爲clientId。之後HTTP請求參數的名稱會從聲明的Action方法中匹配:

public static void details(String clientId){
       
// ...
}

      在模板代碼解析時,Play會自動生成帶clientId參數的URL地址:

<form action="/client/details?clientId=3442" method="GET"
     
accept-charset="utf-8">
 ...
</form>
      需要着重介紹的一點是,在使用#{form}標籤以POST方式提交時,自帶了令牌保護機制,也就是說#{form}標籤包含了#{authenticityToken /}的功能:
#{form @Client.create(), method:'POST', id:'creationForm',
       enctype
:'multipart/form-data' }
   
...
#{/form}

      在模板執行時,以上代碼會被會解析爲如下HTML代碼:

<form action="/client/create" id="creationForm" method="POST"
     
accept-charset="utf-8" enctype="multipart/form-data">
<input type="hidden" name="authenticityToken"
       
value="1c6d92fed96200347f06b7c5e1a3a28fa258ef7c">
 ...
</form>

11.#{i18n}

      i18n在模板中的用法比較特殊,不僅可以直接以標籤的形式用於模板語言,也可以直接在JavaScript中調用。可以在JavaScript代碼中使用i18n()方法來訪問本地化消息文件,如i18n('app_title')。

      首先,需要在conf/messages中進行國際化定義:

hello_world=Hello, World !
hello_someone
=Hello %s !

      之後,在模板中就可以插入#{i18n /}標籤來開啓國際化支持了:

#{i18n /}

      在JavaScript中可以調用i18n()方法來進行國際化操作,通過conf/messages中的key獲取當前key的國際化值:

alert(i18n('hello_world'));
alert
(i18n('hello_someone', 'John'));

      還可以通過設置key的值來限制允許使用的國際化值,並且允許使用通配符來設置範圍:

#{i18n keys:['title', 'menu.*'] /}

12.#{include}

      #{include}標籤的作用是在當前模板中導入另一個模板,並且當前模板中的所有變量對導入的模板透明。其具體使用方法如下:

<div id="tree">
    #{include 'tree.html' /}
</div>
      上例代碼中,直接在當前模板中導入了tree.html模板。

13.#{render}

      #{render}標籤的作用是指定模板進行渲染。#{render}標籤的參數是指定渲染模板的路徑,可以是絕對路徑,也可以是相對於/app/views的路徑:

#{render 'Application/other.html'/}

14.#{jsAction}

      #{jsAction}標籤會生成一個返回服務端Action的URL地址的JavaScript函數,它不會自動執行Ajax請求,需要通過返回的URL地址手動執行。具體的應用示例如下:

      首先在conf/routes文件中定義路由:

GET     /users/{id}        Users.show

      之後,就可以在客戶端導入定義好的路由:

<script type="text/javascript">
   
var showUserAction = #{jsAction @Users.show(':id') /}
   
   
var displayUserDetail = function(userId) {
        $
('userDetail').load( showUserAction({id: userId}) )
   
}
</script>
      有關Ajax的具體用法,會在第10.12節進行詳細介紹。

15.#{list}

      #{list}標籤提供非常簡便的操作遍歷對象集合:

<ul>
#{list items:products, as:'product'}
   
<li>${product}</li>
#{/list}
</ul>

      其中#{list items:products, as:'product'}用來遍歷對象集合products,循環體中的每個對象用變量product來引用。在循環體的代碼中可以引用一些預定義的變量,這些變量有固定的名稱,但是會以循環體中的對象變量名作爲前綴,在上面的例子中就是product_Xxx。#{list}標籤的預定義的變量有以下幾種:

  • note_index:表示當前對象在集合中的序號。
  • note_isFirst:表示是否是集合中的第一個對象。
  • note_isLast:表示是否是集合中的最後一個對象。
  • note_parity:表示當前對象在集合中序號的奇偶值,可能是even或odd。
<ul>
#{list items:products, as:'product'}
   
<span class="${product_parity}">${product_index}. ${product}</span>
    ${product_isLast ? '' : '-'}
#{/list}
</ul>

      #{list}標籤中的items參數是可選的,也可以無需聲明參數類型直接寫參數,這樣可以簡化#{list}標籤的用法:

#{list products, as:'product'}
   
<li>${product}</li>
#{/list}

      在#{list}標籤中可以使用Groovy中的range對象,用法與for循環非常類似:

#{list items:0..10, as:'i'}
    $
{i}
#{/list}
#{list items:'a'..'z', as:'letter'}
    $
{letter} ${letter_isLast ? '' : '|' }
#{/list}

      #{list}標籤中as參數也不是必須的,可以用約定的下劃線符“ _ ”作爲默認的變量名:

#{list users}
   
<li>${_}</li>
#{/list}

16.#{option}

      #{option}標籤是HTML中<option>的封裝,其作用是在當前模板位置中插入<option>元素。#{option}標籤的參數只有一個:

  • value:選項的值。
      #{option}標籤的具體使用方法如下:
#{option user.id} ${user.name} #{/option}

      在模板執行時,以上代碼會被會解析爲如下HTML代碼:

<option value="42">jto</option>
      #{option}標籤一般與#{select}標籤配合使用。

17.#{select}

      #{select}標籤是HTML中<select>的封裝,其作用是在當前模板位置中插入<select>元素。#{select}標籤有一個必選的參數:

  • name(必須):設置select元素的name屬性。

      其具體使用方法如下:

#{select 'booking.beds', value:2, id:'select1'}
   
#{option 1}One king-size bed#{/option}
   
#{option 2}Two double beds#{/option}
   
#{option 3}Three beds#{/option}
#{/select}

      在模板執行時,以上代碼會被會解析爲如下HTML代碼:

<select name="booking.beds" size="1" id="select1" >
   
<option value="1">One king-size bed</option>
   
<option value="2" selected="selected">Two double beds</option>
   
<option value="3">Three beds</option>
</select>

      在#{select}標籤中使用items屬性可以自動生成所需的option選擇項:

  • items(可選):設置集合對象。
  • value(可選):設置默認選中項。
  • labelProperty(可選):設置每一項的lable值對應對象的哪個屬性。
  • valueProperty(可選):設置每一項的value值對應對象的哪個屬性,默認爲對象的id屬性。

注意:

labelProperty和valueProperty屬性值不支持Java基本類型,需使用封裝類型如Integer或Long來代替int或long類型 。


      以下代碼爲#{select}標籤中使用items屬性的例子。首先由控制器Action向頁面模板注入用戶集合List<User> users,且每一個用戶user都有name屬性,id是User的主鍵:

#{select 'users', items:users, valueProperty:'id', labelProperty:'name', value:5, class:'test', id:'select2' /}

      在模板執行時,以上代碼會被會解析爲如下HTML代碼:

<select name="users" size="1" class="test" id="select2" >
 
<option value="0" >User-0</option>
 
<option value="1" >User-1</option>            
 
<option value="2" >User-2</option>            
 
<option value="3" >User-3</option>
 
<option value="4" >User-4</option>
 
<option value="5" selected="selected">User-5</option>
</select>

18.#{set},#{get}

      #{set}標籤用於設置可以在模板中使用的變量。如#{set email:'[email protected]'},就設置了模板變量email的值,可以通過 #{get 'email'} 來獲取。#{set}標籤的定義中可以引用其他變量:

#{set title:'Profile of ' + user.login /}

      還可以在#{set}標籤體內定義變量的值:

#{set 'title'}
   
Profile of ${user.login}
#{/set}

      #{get}標籤的作用是獲取由#{set}標籤定義的值,在Play的模板中,通過get/set機制,可以使父模板和子模板間進行通信。

<head>
   
<title>#{get 'title' /}
</head>

      可以在#{get}標籤體內設置當變量不存在時的缺省值,如下例當title變量不存在時,會顯示Homepage。

<head>
   
<title>#{get 'title'}Homepage #{/}
</head>

19.#{script}

      #{script}標籤用於生成一個<script>元素,可以引入/public/javascripts目錄下的JavaScript文件。如 #{script 'jquery.js'}就引用了/public/javascripts/jquery.js 文件。

#{script src:'jquery-1.5.1.min.js ', id:'jquery' , charset:'utf-8' /}
      #{script}標籤有以下幾類參數:
  • src(必須):設置腳本文件的地址,無需在地址前加上默認父目錄/public/javascripts。
  • id(可選):設置生成的<script>元素的id。
  • charset(可選):設置腳本文件的字符編碼,默認爲UTF-8。
      如果只需要在#{script}標籤中設置src的參數值,則可以省略src參數名,直接寫上需要引入的JavaScript文件即可:
#{script 'jquery-1.5.1.min.js' /}

20.#{stylesheet}

      #{stylesheet}標籤的使用方法與#{script}類似,不同的是#{stylesheet}標籤使用<link>元素來引入/public/stylesheets目錄下的CSS文件。如#{stylesheet id:'main', media:'print', src:'print.css', title:'Print stylesheet' /}就引用了/public/stylesheets/print.js 文件。

      #{stylesheet}標籤有以下幾類參數:

  • src(必須):設置樣式文件的地址,無需在地址前加上默認父目錄/public/stylesheets。
  • id(可選):設置生成的<link>標籤的id。
  • media(可選):設置 media 屬性: screen,print,aural,projection……
  • title(可選):設置標籤title屬性。

      如果只需要在#{stylesheet}標籤中設置src的參數值,則可以省略src參數名,直接寫上需要引入的CSS文件即可:

#{stylesheet 'default.css' /}

21.#{verbatim}

      使用#{verbatim}標籤可以禁用模板中的HTML轉義功能。如果直接按照如下方式在模板中輸出表達式:

${'&amp;'} 
      經模板解析後,會默認轉義輸出“&amp ;”。如果使用了#{verbatim}標籤:
#{verbatim}${'&amp;'}#{/verbatim}
      模板解析時將不會進行轉義,直接輸出“&”。

5.3.2 自定義tag#

      Play提供了模板標籤的自定義功能,可以非常方便地爲應用創建特定的標籤。這不僅提高了代碼重用性,也使模板中的代碼更加整潔。在這裏提到的標籤,指的就是簡單的模板文件(存放在app/views/tags目錄中),在使用時標籤的名稱需要與模板的文件名保持一致。

      具體使用方法如下。在app/views/tags目錄中新建hello.html文件,即成功創建了hello標籤,之後編輯hello.html中的內容:

Hello from tag!

      並沒有進行任何配置,就完成了標籤的自定義,之後就可以在其他模板中直接使用該標籤了:

#{hello /}


獲取標籤參數

      在標籤中可以包含參數,框架會自動將參數傳遞給模板變量。在使用中,標籤的參數名與模板變量名並不是完全一致的,模板變量聲明時需要增加下劃線_。例如,在hello.html標籤模板文件中進行如下聲明:

Hello ${_name}!

      在其他模板中,就可以通過如下語句將name參數傳遞給標籤了:

#{hello name:'Bob' /}

      如果標籤中只有一個參數,就可以採用默認的參數名稱arg。由於該名稱是內置的,在使用時可以不顯式地寫出參數名。例如,在hello.html標籤模板文件中聲明:

Hello ${_arg}!

      在其他模板中,簡單地使用如下語句就可以將Bob傳遞給標籤:

#{hello 'Bob' /}


調用標籤體

      起始標籤和結束標籤之間包含的內容稱爲標籤體,標籤體可以是文本,HTML等。在Play中調用#{doBody /}標籤可以很方便地完成以上內容。例如,在hello.html標籤模板文件中聲明:

Hello #{doBody /}!

      之後Ben將會作爲標籤體進行傳遞:

#{hello}
   
Ben
#{/hello}


自動選擇最適標籤

      根據content type設置,不同的頁面可能會由不同的格式進行渲染(HTML,XML,JSON等)。這一點在Play中完全不用擔心,框架會選取最適合的形式來顯示。例如,當請求希望以HTML格式渲染時,Play會自動調用app/views/tags/hello.html模板;當請求希望以XML格式渲染時,Play則會選擇app/views/tags/hello.xml模板。

      在Play中可以定義後綴爲.tag的通用模板標籤,但他的優先級是最低的。只有當規定的模板標籤格式是無效時,纔會調用它。例如,在程序中只定義了兩個模板標籤,分別是app/views/tags/hello.xml和app/views/tags/hello.tag,當請求希望以HTML格式渲染時,框架會優先尋找hello.html模板,當發現該文件不存在時,纔會調用hello.tag模板。

5.3.3 自定義Play標籤#

      也可以在Java代碼中自定義標籤,封裝一些更爲複雜的功能。如果希望自定義Java標籤,需要先創建繼承於play.templates.FastTags的類。
package utils;

public class MyFastTag extends FastTags {
   
...
}
      每個想要作爲標籤(tag)運行的方法都具有固定的聲明方式,具體如下:
public static void _tagName(Map<?, ?> args, Closure body, PrintWriter out, 
   
ExecutableTemplate template, int fromLine)


注意:

別漏了標籤名前面的下劃線。


      爲了進一步瞭解如何創建標籤,先來看看兩個Play內置標籤的例子(Play封裝了這兩個標籤,在play/framework/src/play/templates/FastTags.java文件中可以查看)。

      #{verbatim}標籤只是簡單的調用了從JavaExtensions傳遞過來的toString()方法,直接在模板上打印了標籤體中的內容:

public static void _verbatim(Map<?, ?> args, Closure body, PrintWriter out, 
   
ExecutableTemplate template, int fromLine) {
   
   
out.println(JavaExtensions.toString(body));
}

      該標籤的具體使用方法如下,從標籤的開始到結束都是標籤體內容:

#{verbatim}My verbatim #{/verbatim}

      模板中將會打印出標籤體內容:

My verbatim
      第二個Play內置標籤例子是#{option},它要稍微複雜一點,因爲它的實現依賴於父標籤#{select}(Play也封裝了#{select}標籤,在play/framework/templates/tags/select.tag文件中可以查看具體定義)。
public static void _option(Map<?, ?> args, Closure body, PrintWriter out, 
     
ExecutableTemplate template, int fromLine) {
 
   
Object value = args.get("arg");
   
Object selection = TagContext.parent("select").data.get("selected");
   
boolean selected = selection != null && value != null
     
&& selection.equals(value);
 
   
out.print("<option value=\"" + (value == null ? "" : value) + "\" "
     
+ (selected ? "selected=\"selected\"" : "")
     
+ "" + serialize(args, "selected", "value") + ">");
   
out.println(JavaExtensions.toString(body));
   
out.print("</option>");
}

      該Java標籤的原理是封裝了HTML的<option>標籤,用於實現下拉列表實例,並且從父標籤#{select}中獲取被默認選中(selected)的值。方法體最開始的三行用於設定輸出的變量,最後的三行則輸出了標籤的結果。該標籤的具體使用方法如下:

#{select name:"country",value:"中國"}
   
#{option arg:"英國"}英國#{/option}
   
#{option arg:"中國"}中國#{/option}
   
#{option arg:"美國"}美國#{/option}
   
#{option arg:"日本"}日本#{/option}
#{/select}

      這裏需要對父標籤#{select}的使用做一些簡單說明:country是#{select}標籤的name屬性,是必選的;中國是#{select}標籤的value屬性,用於標識selected元素,是可選的。


標籤的命名空間

      實際項目中可能會自定義大量的標籤,標籤命名不規範可能會造成一些不必要的麻煩(比如與已定義的標籤重名,或是與Play框架的核心標籤衝突)。爲了解決這個問題,Play可以使用@FastTags.Namespace註解來設定命名空間。對於my.tags命名空間中的#{hello}標籤需要做以下工作:

@FastTags.Namespace("my.tags") 
public class MyFastTag extends FastTags {
   
public static void _hello (Map<?, ?> args, Closure body, PrintWriter out,
     
ExecutableTemplate template, int fromLine) {
     
...
   
}
}

      然後在模板中可以通過以下方式來調用#{hello}標籤:

#{my.tags.hello/}
發佈了8 篇原創文章 · 獲贊 3 · 訪問量 1萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章