现在的位置: 首页 > 综合 > 正文

脚本化文档

2013年02月01日 ⁄ 综合 ⁄ 共 18525字 ⁄ 字号 评论关闭
文章目录

1、动态文档内容

使用document.write()有两种方式。第一种方式是,在脚本中使用它,把HTML输入到当前正在被解析的文档中。但要注意的是,只能在当前文档正在解析时使用write()方法向其输出HTML代码。也就是说,只能在标记<script>的顶层代码中调用方法document.write(),因为这些脚本的执行是文档解析过程的一部分。如果把一个document.write()调用放入到一个函数定义中,然后从一个事件句柄中调用doucment.write(),它不会像期望的那样工作;实际上,它将会覆盖当前文档和它所包含的脚本。

也可以使用write()方法来在其他的窗口或帧中创建一个全新的文档。虽然不能有效地从事件句柄中改写当前文档,但是可以把文档写入另一个窗口或帧,这在多窗口或多帧的网站中非常有用。例如,可以创建一个弹出窗口并用如下代码为其编写一些HTML。要创建新文档,首先需要调用Document对象的open()方法,然后多次调用write()方法在文档中写入内容,最后调用Document对象的方法close()以说明创建过程结束了。

2、Document属性

看完了Document的遗留方法,现在看看它的遗留属性:

bgColor

文档的背景颜色。这个属性对应于标记<body>的bgcolor属性(已经废弃)。

cookie

一个特殊属性,允许JavaScript程序读写HTTP cookie。

domain

该属性使位于同一Internet域中的相互信任的Web服务器在它们的网页间交互时能协同地放松同源策略安全性限制。

lastModified

一个字符串,包含文档的修改日期。

location

等价于属性URL,已经废弃。

referrer

文档的URL,包含吧浏览器带到当前文档的链接(如果存在这样的链接)。

title

位于文档的标记<title>和</title>之间的文本。

URL

一个字符串。声明了装载文档的URL。除非发生了服务器重定向,否则该属性的值与Window对象的location.href相同。

3、遗留DOM:文档对象集合

前面一节中的Document对象列表中漏掉了重要的一类属性,就是文档对象集合。这些值为数组的属性是遗留DOM的核心。这些属性使得可以访问文档的某些具体元素:

anchors[]

Anchor对象的一个数组,该对象代表文档中的锚。一个Anchor对象的name属性保存了name属性的值。

applets[]

Applet对象的一个数组,该对象代表文档中的Java applet。

forms[]

Form对象的一个数组,该对象代表文档中的<form>元素。每个Form对象都有自己的一个名为elements[]的集合属性,其中包含了代表表单中所包含的表单元素的对象。在表单提交之前,Form对象触发一个onsubmit事件句柄。这个句柄可以执行客户端的表单验证:如果它返回false,浏览器将不会提交表单。

images[]

Image对象的一个数组,该对象代表文档中的<img>元素。Image对象的src属性是可读写的,并且给这个属性赋一个URL会导致浏览器读取和显示一个新的图像。脚本化一个Image对象的src属性可以实现图像翻滚效果和简单的动画。

links[]

Link对象的一个数组,该对象是代表文档中的超文本链接的Link对象。超文本链接是在HTML中用<a>标记创建的,并且偶尔会用客户端图像地图的<area>标记来创建。一个Link对象的href属性和<a>标记的href属性向对应:它保存了该链接的URL。Link对象也通过protocol、hostname和pathname等属性使一个URL的不同部分变得可用。

3.1、命名Document对象

使用数字来索引文档对象集合的一个问题就是,基于位置的索引不稳定,小的文档改变了文档元素的顺序就可能会破坏基于它们的顺序的代码。更加健壮的解决方案是为重要的的文档元素分配名字,并且用名字来引用这些元素。而且应尽量确保name属性是唯一的。

3.2、Document对象上的事件句柄

在HTML中,通过把JavaScript代码赋予一个事件句柄属性就可以定义事件句柄。但在JavaScript中,则通过把函数赋予一个事件句柄属性来定义它们。如:

<form name="myform" onsubmit="return validateform();">...</form>

在JavaScript中,除了使用JavaScript代码串调用函数并返回它的值,还可以直接把函数赋予事件句柄属性,如下所示:

document.myform.onsubmit = validateform;

注意,函数后没有括号,这是因为此处我们不想调用函数,只想把一个引用赋予它。

3.3、遗留DOM示例

/*
 * listanchors.js: Create a simple table-of-contents with document.anchors[].
 * 
 * The function listanchors() is passed a document as its argument and opens
 * a new window to serve as a "navigation window" for that document. The new 
 * window displays a list of all anchors in the document. Clicking on any 
 * anchor in the list causes the document to scroll to that anchor.
 */
function listanchors(d) {
    // Open a new window
    var newwin = window.open("", "navwin", 
                             "menubar=yes,scrollbars=yes,resizable=yes," +
                             "width=500,height=300");

    // Give it a title
    newwin.document.write("<h1>Navigation Window: " + d.title + "</h1>");

    // List all anchors
    for(var i = 0; i < d.anchors.length; i++) {
        // For each anchor object, determine the text to display. 
        // First, try to get the text between <a> and </a> using a 
        // browser-dependent property. If none, use the name instead.
        var a = d.anchors[i];
        var text = null;
        if (a.text) text = a.text;                          // Netscape 4
        else if (a.innerText) text = a.innerText;           // IE 4+
        if ((text == null) || (text == '')) text = a.name;  // Default

        // Now output that text as a link. The href property of this link
        // is never used: the onclick handler does the work, setting the 
        // location.hash property of the original window to make that
        // window jump to display the named anchor.  See Window.opener,
        // Window.location and Location.hash, and Link.onclick.
        newwin.document.write('<a href="#' + a.name + '"' +
                              ' onclick="opener.location.hash=\'' + a.name + 
                              '\'; return false;">'); 
        newwin.document.write(text);
        newwin.document.write('</a><br>');
    }
    newwin.document.close();  // Never forget to close the document!
}

4、W3C DOM概览

4.1、把文档表示为树

HTML文档有一个嵌入标记的层级结构,它在DOM中表示对象的一棵树。HTML文档的这个树的表示包含了表示HTML标记和元素的节点,以及表示文本的串的节点。HTML文档也可以包含表示HTML注释的节点。

4.2、节点

4.2.1、节点的类型

文档树中的不同类型的节点都用节点的特定的子接口来表示。每个Node对象都有一个nodeType属性,它指定了节点是什么类型的。下表列出了在HTML文档中常见的节点类型以及每种类型的nodeType值:

接口 nodeType常量 nodeType值
Element Node.ELEMENT_NODE 1
Text Node.TEXT_NODE 3
Document Node.DOCUMENT_NODE 9
Comment Node.COMMENT_NODE 8
DocumentFragment Node.DOCUMENT_FRAGMENT_NODE 11
Attr Node.ATTRIBUTE_NODE 2

DOM树根部的Node是一个Document对象。这个对象的documentElement属性引用了一个Element对象,它代表了文档的根元素。对于HTML文档,这是<html>标记,它在文档中可以是显示的或隐式的。

在一个DOM树中只有一个Document对象。DOM树的大部分节点是表示标记的Element对象和表示文本串的Text对象。如果文档解析器保留了注释,那么这些注释在DOM树中由Comment对象表示。

4.2.2、属性

用Element接口的getAttribute()方法、SetAttribute()方法和removeAttribute()方法可以查询、设置并删除一个元素的属性。

另一种使用属性的方式是调用getAttributeNode()方法,它将返回一个表示属性和它的值的Attr对象。

4.3、DOM HTML API

如下是简单的HTML标记:

<abbr> <acronym> <address> <b> <bdo>
<big> <center> <cite> <code> <dd>
<dfn> <dt> <em> <i> <kbd>
<noframes> <noscript> <s> <samp> <small>
<span> <strike> <strong> <sub> <sup>
<tt> <u> <var>    

4.4、DOM级别和特性

4.5、DOM一致性

4.6、独立于语言的DOM接口

5、遍历文档

遍历文档的节点:

<head>
<script>
// This function is passed a DOM Node object and checks to see if that node 
// represents an HTML tag, i.e., if the node is an Element object. It
// recursively calls itself on each of the children of the node, testing
// them in the same way. It returns the total number of Element objects
// it encounters. If you invoke this function by passing it the
// Document object, it traverses the entire DOM tree.
function countTags(n) {                         // n is a Node 
    var numtags = 0;                            // Initialize the tag counter
    if (n.nodeType == 1 /*Node.ELEMENT_NODE*/)  // Check if n is an Element
        numtags++;                              // Increment the counter if so
    var children = n.childNodes;                // Now get all children of n
    for(var i=0; i < children.length; i++) {    // Loop through the children
        numtags += countTags(children[i]);      // Recurse on each one
    }
    return numtags;                             // Return the total
}
</script>
</head>
<!-- Here's an example of how the countTags() function might be used -->
<body onload="alert('This document has ' + countTags(document) + ' tags')">
This is a <i>sample</i> document.
</body>

获取一个DOM节点下的所有文本:

/** 
 * getText(n): Find all Text nodes at or beneath the node n.
 * Concatenate their content and return it as a string.
 */
function getText(n) {
    // Repeated string concatenation can be inefficient, so we collect
    // the value of all text nodes into an array, and then concatenate
    // the elements of that array all at once.
    var strings = [];
    getStrings(n, strings);
    return strings.join("");

    // This recursive function finds all text nodes and appends
    // their text to an array.
    function getStrings(n, strings) {
        if (n.nodeType == 3 /* Node.TEXT_NODE */) 
            strings.push(n.data);
        else if (n.nodeType == 1 /* Node.ELEMENT_NODE */) {
            // Note iteration with firstChild/nextSibling
            for(var m = n.firstChild; m != null; m = m.nextSibling) {
                getStrings(m, strings);
            }
        }
    }
}

6、在文档中查找元素

document.documentElement属性引用了作为文档的根元素的<html>标记。而document.body属性引用了<body>标记。如果HTMLDocument对象的body属性不存在,也可以用如下引用<body>标记:

document.getElementsByTagName("body")[0];

这个表达式调用Document对象的getElementByTagName()方法,选择了返回的数组的第一个元素。

最后要注意,对于HTML文档来说,HTMLDocument对象还定义了一个getElementByName()方法。该方法与getElementById()方法相似,但它查询元素的name属性,而不是id属性。

用类名或标记名选择HTML元素:

/**
 * getElements(classname, tagname, root):
 * Return an array of DOM elements that are members of the specified class,
 * have the specified tagname, and are descendants of the specified root.
 * 
 * If no classname is specified, elements are returned regardless of class.
 * If no tagname is specified, elements are returned regardless of tagname.
 * If no root is specified, the document object is used.  If the specified
 * root is a string, it is an element id, and the root
 * element is looked up using getElementsById()
 */
function getElements(classname, tagname, root) {
    // If no root was specified, use the entire document
    // If a string was specified, look it up
    if (!root) root = document;
    else if (typeof root == "string") root = document.getElementById(root);
    
    // if no tagname was specified, use all tags
    if (!tagname) tagname = "*";

    // Find all descendants of the specified root with the specified tagname
    var all = root.getElementsByTagName(tagname);

    // If no classname was specified, we return all tags
    if (!classname) return all;

    // Otherwise, we filter the element by classname
    var elements = [];  // Start with an emtpy array
    for(var i = 0; i < all.length; i++) {
        var element = all[i];
        if (isMember(element, classname)) // isMember() is defined below
            elements.push(element);       // Add class members to our array
    }

    // Note that we always return an array, even if it is empty
    return elements;

    // Determine whether the specified element is a member of the specified
    // class.  This function is optimized for the common case in which the 
    // className property contains only a single classname.  But it also 
    // handles the case in which it is a list of whitespace-separated classes.
    function isMember(element, classname) {
        var classes = element.className;  // Get the list of classes
        if (!classes) return false;             // No classes defined
        if (classes == classname) return true;  // Exact match

        // We didn't match exactly, so if there is no whitespace, then 
        // this element is not a member of the class
        var whitespace = /\s+/;
        if (!whitespace.test(classes)) return false;

        // If we get here, the element is a member of more than one class and
        // we've got to check them individually.
        var c = classes.split(whitespace);  // Split with whitespace delimiter
        for(var i = 0; i < c.length; i++) { // Loop through classes
            if (c[i] == classname) return true;  // and check for matches
        }

        return false;  // None of the classes matched
    }
}

7、修改一个文档

虽然遍历一个文档的节点很有用,但DOM核心API的真正威力在于它能用JavaScript动态修改文档的特性。

下例包括一个名为sortkids()的JavaScript函数、一个示例文档和一个HTML按钮。在该按钮被按下时,将调用sortkids()函数,并传递给它表示一个<ul>标记的ID。sortkids()函数找到给定节点的子元素,根据它们所包含的文本来对其排序,并在文档中重新排列它们,以使它们按照字母顺序出现。

<script>
function sortkids(e) {
     // This is the element whose children we are going to sort
     if (typeof e == "string") e = document.getElementById(e);

     // Transfer the element (but not text node) children of e to a real array
     var kids = [];
     for(var x = e.firstChild; x != null; x = x.nextSibling)
         if (x.nodeType == 1 /* Node.ELEMENT_NODE */) kids.push(x);

     // Now sort the array based on the text content of each kid.
     // Assume that each kid has only a single child and it is a Text node
     kids.sort(function(n, m) { // This is the comparator function for sorting
                   var s = n.firstChild.data; // text of node n
                   var t = m.firstChild.data; // text of node m
                   if (s < t) return -1;      // n comes before m
                   else if (s > t) return 1;  // n comes after m
                   else return 0;             // n and m are equal
               });

     // Now append the kids back into the parent in their sorted order.
     // When we insert a node that is already part of the document, it is
     // automatically removed from its current position, so reinserting
     // these nodes automatically moves them from their old position
     // Note that any text nodes we skipped get left behind, however.
     for(var i = 0; i < kids.length; i++) e.appendChild(kids[i]);
}
</script>

<ul id="list"> <!-- This is the list we'll sort -->
<li>one<li>two<li>three<li>four <!-- items are not in alphabetical order -->
</ul>
<!-- this is the button that sorts the list -->
<button onclick="sortkids('list')">Sort list</button>

下面例子通过改变文档的文本而改变其内容,它定义了一个upcase()函数,递归地从一个具体的Node向下遍历,并将它所发现的任何Text节点的内容变为大写的。

// This function recursively looks at Node n and its descendants, 
// converting all Text node data to uppercase
function upcase(n) {
    if (n.nodeType == 3 /*Node.TEXT_NODE*/) {
        // If the node is a Text node, create a new Text node that
        // holds the uppercase version of the node's text, and use the
        // replaceChild() method of the parent node to replace the
        // original node with the new uppercase node.
        n.data = n.data.toUpperCase();
    }
    else {
        // If the node is not a Text node, loop through its children
        // and recursively call this function on each child.
        var kids = n.childNodes;
        for(var i = 0; i < kids.length; i++) upcase(kids[i]);
    }
}

下面的例子定义了名为embolden()函数,该函数用表示HTML标记<b>的新元素(由Document对象的createElement()方法创建)替代指定的节点,并改变原始节点的父节点,使它成为新的<b>节点的子节点。在一个HTML文档中,这会使该节点中的所有文本或它的子孙以粗体显示。

<script>
// This function takes a Node n, replaces it in the tree with an Element node
// that represents an HTML <b> tag, and then makes the original node the
// child of the new <b> element.
function embolden(n) {
    if (typeof n == "string") n = document.getElementById(n); // Lookup node
    var b = document.createElement("b"); // Create a new <b> element
    var parent = n.parentNode;           // Get the parent of the node
    parent.replaceChild(b, n);           // Replace the node with the <b> tag
    b.appendChild(n);                    // Make the node a child of the <b>
}
</script>

<!-- A couple of sample paragraphs -->
<p id="p1">This <i>is</i> paragraph #1.</p>
<p id="p2">This <i>is</i> paragraph #2.</p>

<!-- A button that invokes the embolden() function on the element named p1 -->
<button onclick="embolden('p1');">Embolden</button>

7.1、修改属性

除了用改变文本和重排节点的方式修改文档外,还可以通过设置文档元素的属性对文档进行较大的修改。一种方法是调用element.setAttribute()。例如

var headline = document.getElementById("headline");

headline.setAttribute("align", "center");

表示HTML属性的DOM元素定义了对应于每个标准属性的JavaScript属性(即使是被废弃使用的属性,如align),所以也可以用这段代码实现同样的效果:

var headline = document.getElementById("headline");

headline.align = "center";

7.2、使用Document段

DocumentFragment是一种特殊类型的节点,它本身不出现在文档中,只作为连续节点集合的临时容器,并允许将这些节点作为一个对象来操作。当把一个DocumentFragment插入文档时,插入的不是它本身,而是它的所有子节点。

可以用document.createDocumentFragment()来创建一个DocumentFragment。创建了DocumentFragment后,就可以用下列代码使用它。可以用appendChild()或任何相关的Node方法来为一个DocumentFragment添加节点。在这样做了之后,段还是空的并且无法复用,除非首先为它添加一个孩子节点。下面的例子展示了这一过程,它定义了一个reverse()函数,在反转一个Node的孩子的顺序的时候,它使用一个DocumentFragment作为临时容器。

// Reverse the order of the children of Node n
function reverse(n) { 
    // Create an empty DocumentFragment as a temporary container
    var f = document.createDocumentFragment(); 

    // Now loop backward through the children, moving each one to the fragment.
    // The last child of n becomes the first child of f, and vice-versa.
    // Note that appending a child to f automatically removes it from n.
    while(n.lastChild) f.appendChild(n.lastChild); 

    // Finally, move the children of f all at once back to n, all at once.
    n.appendChild(f);
}

8、给文档添加内容

Document.createElement()方法和Document.createTextNode()方法创建新的Element节点和Text节点,而方法Node.appendChild()、Node.insertBefore()和Node.replaceChild()可以用来将它们添加到一个文档。有了这些方法,就可以够将任意文档内容的一个DOM树。

下面就是一个拓展后的示例,它定义了一个log()函数用来记录消息和对象。

/*
 * Log.js: Unobtrusive logging facility
 *
 * This module defines a single global symbol: a function named log().
 * Log a message by calling this function with 2 or 3 arguments:
 * 
 *   category: the type of the message.  This is required so that messages
 *     of different types can be selectively enabled or disabled and so
 *     that they can be styled independently.  See below.
 *
 *   message: the text to be logged.  May be empty if an object is supplied
 *
 *   object: an object to be logged.  This argument is optional.  If passed,
 *     the properties of the object will be logged in the form of a table.
 *     Any property whose value is itself an object may be logged recursively.
 * 
 * Utility Functions:
 *
 *   The log.debug() and log.warn() functions are utilities that simply 
 *   call the log() function with hardcoded categories of "debug" and
 *   "warning".  It is trivial to define a utility that replaces the built-in
 *   alert() method with one that calls log().
 * 
 * Enabling Logging
 *
 *   Log messages are *not* displayed by default.  You can enable the
 *   display of messages in a given category in one of two ways.  The
 *   first is to create a <div> or other container element with an id
 *   of "<category>_log".  For messages whose category is "debug", you might
 *   place the following in the containing document:
 * 
 *      <div id="debug_log"></div>
 *
 *   In this case, all messages of the specified category are appended
 *   to this container, which can be styled however you like.
 * 
 *   The second way to enable messages for a given category is to
 *   set an appropriate logging option.  To enable the category
 *   "debug", you'd set log.options.debugEnabled = true.  When you
 *   do this, a <div class="log"> is created for the logging messages.
 *   If you want to disable the display of log messages, even if a container
 *   with a suitable id exists, set another option:
 *   log.options.debugDisabled=true.  Set this option back to false to 
 *   reenable log messages of that category.
 *
 * Styling Log Messages
 * 
 *   In addition to styling the log container, you can use CSS to
 *   style the display of individual log messages.  Each log message
 *   is placed in a <div> tag, and given a CSS class of
 *   <category>_message.  Debugging messages would have a class "debug_message"
 * 
 * Log Options
 * 
 *   Logging behavior can be altered by setting properties of the log.options
 *   object, such as the options described earlier to enable or disable logging
 *   for given categories.  A few other options are available:
 *
 *     log.options.timestamp: If this property is true, each log message
 *       will have the date and time added to it.
 *
 *     log.options.maxRecursion: An integer that specifies the maximum number
 *       of nested tables to display when logging objects.  Set this to 0 if
 *       you never want a table within a table.
 *
 *     log.options.filter: A function that filters properties out when logging
 *       an object.  A filter function is passed the name and value of
 *       a property and returns true if the property should appear in the
 *       object table or false otherwise.
 */
function log(category, message, object) {
    // If this category is explicitly disabled, do nothing
    if (log.options[category + "Disabled"]) return;

    // Find the container
    var id = category + "_log";
    var c = document.getElementById(id);

    // If there is no container, but logging in this category is enabled,
    // create the container.
    if (!c && log.options[category + "Enabled"]) {
        c = document.createElement("div");
        c.id = id;
        c.className = "log";
        document.body.appendChild(c);
    }

    // If still no container, we ignore the message
    if (!c) return;

    // If timestamping is enabled, add the timestamp
    if (log.options.timestamp)
        message = new Date() + ": " + (message?message:"");

    // Create a <div> element to hold the log entry
    var entry = document.createElement("div");
    entry.className = category + "_message";

    if (message) {
        // Add the message to it
        entry.appendChild(document.createTextNode(message));
    }

    if (object && typeof object == "object") {
        entry.appendChild(log.makeTable(object, 0));
    }

    // Finally, add the entry to the logging container
    c.appendChild(entry);
}

// Create a table to display the properties of the specified object
log.makeTable = function(object, level) {
    // If we've reached maximum recursion, return a Text node instead.
    if (level > log.options.maxRecursion)
        return document.createTextNode(object.toString());
    
    // Create the table we'll be returning
    var table = document.createElement("table");
    table.border = 1;
    
    // Add a Name|Type|Value header to the table
    var header = document.createElement("tr");
    var headerName = document.createElement("th");
    var headerType = document.createElement("th");
    var headerValue = document.createElement("th");
    headerName.appendChild(document.createTextNode("Name"));
    headerType.appendChild(document.createTextNode("Type"));
    headerValue.appendChild(document.createTextNode("Value"));
    header.appendChild(headerName);
    header.appendChild(headerType);
    header.appendChild(headerValue);
    table.appendChild(header);

    // Get property names of the object and sort them alphabetically
    var names = [];
    for(var name in object) names.push(name);
    names.sort();

    // Now loop through those properties
    for(var i = 0; i < names.length; i++) {
        var name, value, type;
        name = names[i];
        try {
            value = object[name];
            type = typeof value;
        }
        catch(e) { // This should not happen, but it can in Firefox
            value = "<unknown value>";
            type = "unknown";
        };

        // Skip this property if it is rejected by a filter
        if (log.options.filter && !log.options.filter(name, value)) continue;

        // Never display function source code: it takes up too much room
        if (type == "function") value = "{/*source code suppressed*/}";

        // Create a table row to display property name, type and value
        var row = document.createElement("tr");
        row.vAlign = "top";
        var rowName = document.createElement("td");
        var rowType = document.createElement("td");
        var rowValue = document.createElement("td");
        rowName.appendChild(document.createTextNode(name));
        rowType.appendChild(document.createTextNode(type));

        // For objects, recurse to display them as tables
        if (type == "object") 
            rowValue.appendChild(log.makeTable(value, level+1));
        else 
            rowValue.appendChild(document.createTextNode(value));

        // Add the cells to the row, and add the row to the table
        row.appendChild(rowName);
        row.appendChild(rowType);
        row.appendChild(rowValue);

        table.appendChild(row);
    }
    
    // Finally, return the table.
    return table;
}

// Create an empty options object
log.options = {};

// Utility versions of the function with hardcoded categories
log.debug = function(message, object) { log("debug", message, object); };
log.warn = function(message, object) { log("warning", message, object); };

// Uncomment the following line to convert alert() dialogs to log messages
// function alert(msg) { log("alert", msg); }

8.1、创建节点的方便方法

8.2、innerHTML属性

尽管HTMLElement节点的innerHTML属性还没被W3C批准为DOM的正式的一部分,但它是所有现代浏览器都支持的一个重要而功能强大的属性。当查询一个HTML元素的这一个属性值的时候,所得到的是该元素的孩子的一个HTML文本字符串。如果设置这个属性,浏览器就会调用自己的HTML解析器来解析字符串,并且用解析其所返回的结果来替换该元素的孩子。

把一个HTML文档描述成一串HTML文本,通常比将它描述为对createElement()和appendChild()的一系列的调用要更加方便和紧凑。

使用innerHTML是可以做的相当高效的事情,尤其针对大量需要解析的HTML的时候。但是,注意,使用+=运算符为innerHTML属性附加一些文本通常效率不高,因为它既需要序列化的步骤也需要解析步骤。

抱歉!评论已关闭.