ASP.NET Core 中文文档 第四章 MVC(3.6.2 )自定义标签辅助类(Tag Helpers)

时间:2022-04-04 18:20:25

原文:Authoring Tag Helpers

作者:Rick Anderson

翻译:张海龙(jiechen)

校对:许登洋(Seay)

示例代码查看与下载

从 Tag Helper 讲起

本篇教程是对 Tag Helper 编程作以介绍。 Tag Helpers 介绍 描述了 Tag Helper 的优势。

Tag Helper 是任何实现 ITagHelper 接口的类(Class)。然而,当你编写一个 Tag Helper,你通常是从 TagHelper 开始,这样做让你可以访问 Process 方法。我们将介绍 TagHelper 方法和属性,如同我们将在本教程使用它们的。

创建一个命名为 AuthoringTagHelpers 的新 ASP.NET Core 项目。对该项目你不需要添加身份验证。

创建一个用来放置 Tag Helper 的 TagHelpers 文件夹。 TagHelpers 文件夹是 必需的,但它是一个合理的惯例。现在让我们来开始编写一些简单的 Tag Helper。

编写 email Tag Helper

这一节我们将写一个 Tag Helper ,用来更新 email 标签。例如:

<email>Support</email>

服务端将使用我们的 email Tag Helper 来生成以下标记:

<a href="mailto:Support@contoso.com">Support@contoso.com</a>

也就是,一个锚标签转为了一个 email 链接。如果你在写一个博客引擎,并且需要它为市场、支持、其他联系人发送邮件到相同的域,你可能想要这样做。

1.添加下面的 EmailTagHelper 类到 TagHelpers 文件夹。

using Microsoft.AspNetCore.Razor.TagHelpers;
using System.Threading.Tasks; namespace AuthoringTagHelpers.TagHelpers
{
public class EmailTagHelper : TagHelper
{
public override void Process(TagHelperContext context, TagHelperOutput output)
{
output.TagName = "a"; // Replaces <email> with <a> tag
}
}
}

说明:

  • Tag helper 使用以目标元素名作为根类名(除去类名中 TagHelper 部分)的命名约定。在这个例子中, EmailTagHelper 的根名称是 email ,因此 <email> 标签将是目标标签。这个命名约定适用于大多数 tag helper ,稍后我将展示如何对它重写。
  • EmailTagHelper 类派生自 TagHelperTagHelper 类提供了我们即将在本文探究的丰富的方法和属性。
  • 重写 Process 方法可以控制 Tag Helper 在执行过程中的行为。 TagHelper 类同样提供了相同参数的异步版本(ProcessAsync)。
  • Process (或 ProcessAsync)的上下文参数包含了与当前 HTML 标签执行的相关信息。
  • Process (或 ProcessAsync)的输出参数包含了用来生成 HTML 标签和内容的源代码的静态 HTML 元素呈现。
  • 我们的类名后缀为 TagHelper ,是 必需的,但它被认为是最佳惯例约定。你可以定义类,如:
public class Email : TagHelper

2.为使 EmailTagHelper 类在我们所有 Razor 视图中可用,我们将把 addTagHelper 指令添加到 Views/_ViewImports.cshtml 文件:

@using AuthoringTagHelpers
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper "*, AuthoringTagHelpers"

以上代码我们使用了通配符表明所有的 tag helper 都将在我们的程序集中启用。 @addTagHelper 之后的第一个字符串指明了要加载的 tag helper(我们使用 “*” 代表所有 tag helper ),第二个字符串 “AuthoringTagHelpers” 指明了此 tag helper 所在的程序集。除此之外要注意的是,使用通配符的第二行,引入了 ASP.NET Core MVC 的 tag helper(这些辅助类在 Tag Helpers 介绍中已经讨论过)。是 @addTagHelper 命令使 tag helper 在 Razor 视图中起作用的。你还可以提供如下所示的 tag helper 的全名(FQN):

@using AuthoringTagHelpers
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper "AuthoringTagHelpers.TagHelpers3.EmailTagHelper, AuthoringTagHelpers"

使用 FQN 给视图添加 tag helper,首先你要添加 FQN(AuthoringTagHelpers.TagHelpers.EmailTagHelper),然后是程序集名称(AuthoringTagHelpers)。多数开发人员喜欢用通配符。Tag Helpers 介绍 详细了解 tag helper 的添加、删除、层次结构和通配符。

3.更新 Views/Home/Contact.cshtml 文件中下列变化对应的标签。

@{
ViewData["Title"] = "Contact";
}
<h2>@ViewData["Title"].</h2>
<h3>@ViewData["Message"]</h3> <address>
One Microsoft Way<br />
Redmond, WA 98052<br />
<abbr title="Phone">P:</abbr>
425.555.0100
</address> <address>
<strong>Support:</strong><email>Support</email><br />
<strong>Marketing:</strong><email>Marketing</email>
</address>

4.运行应用并使用你喜欢的浏览器查看 HTML 代码,你可以校验 email 标签都被替换成了链接标签(例如: <a>Support</a>),SupportMarketing 被渲染为链接,但是,它们没有一个 href 属性能使其正常运行。我们将在下一节修复它。

说明: 比如 HTML 标签与属性,Razor 与 C# 中的标签、类名及属性是不区分大小写的。

一个可工作的 email Tag Helper

在这一节中,我们将更新 EmailTagHelper 使其可以为 email 创建一个有效的锚链接标签。我们将修改我们的 tag helper 使其在 Razor 视图中附加信息(以 mail-to 属性的形式)并使用它生成链接。

参照以下代码更新 EmailTagHelper

public class EmailTagHelper : TagHelper
{
private const string EmailDomain = "contoso.com"; // Can be passed via <email mail-to="..." />.
// Pascal case gets translated into lower-kebab-case.
public string MailTo { get; set; } public override void Process(TagHelperContext context, TagHelperOutput output)
{
output.TagName = "a"; // Replaces <email> with <a> tag var address = MailTo + "@" + EmailDomain;
output.Attributes.SetAttribute("href", "mailto:" + address);
output.Content.SetContent(address);
}
}

说明:

  • 以 Pascal 形式命名 tag helper 的类名及属性名会被翻译成它们的 小写 kebab 形式。因此,你使用 MailTo 属性,与使用 <email mail-to="value"/> 是等价的。
  • 最后一行设置了我们 tag helper 完成的最小化功能的内容。
  • 以下代码展示添加属性的语法:
public override void Process(TagHelperContext context, TagHelperOutput output)
{
output.TagName = "a"; // Replaces <email> with <a> tag var address = MailTo + "@" + EmailDomain;
output.Attributes.SetAttribute("href", "mailto:" + address);
output.Content.SetContent(address);
}

虽然当前 “href” 在属性集中不存在,但离实现已经很接近了。你同样可以使用 output.Attributes.Add 方法在标签属性集的最后添加一个 tag helper 属性。

3.依照以下改动修改 Views/Home/Contact.cshtml 文件标记:

@{
ViewData["Title"] = "Contact Copy";
}
<h2>@ViewData["Title"].</h2>
<h3>@ViewData["Message"]</h3> <address>
One Microsoft Way Copy Version <br />
Redmond, WA 98052-6399<br />
<abbr title="Phone">P:</abbr>
425.555.0100
</address> <address>
<strong>Support:</strong><email mail-to="Support"></email><br />
<strong>Marketing:</strong><email mail-to="Marketing"></email>
</address>

4.运行应用可验证它生成了正确的链接。

说明: 如果你写的是自闭合的 email 标签(<email mail-to="Rick" />),最终的输出也将是自闭合的。为了启用写入仅是一个开始标签的功能( <email mail-to="Rick"> ),你必须如下设置类:

[HtmlTargetElement("email", TagStructure = TagStructure.WithoutEndTag)]

使用自闭合的 email tag helper,输出将是 <a href="mailto:Rick@contoso.com" />。自闭合链接标签是无效的 HTML,因此你不应该创建,但你可能想要创建自闭合的 tag helper。Tag helper 是在读取 tag 后设置 TagMode 属性的。

异步 email helper

这一节我们将编写一个异步 email helper。

1.用以下代码替换 EmailTagHelper 类:

public class EmailTagHelper : TagHelper
{
private const string EmailDomain = "contoso.com";
public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
output.TagName = "a"; // Replaces <email> with <a> tag
var content = await output.GetChildContentAsync();
var target = content.GetContent() + "@" + EmailDomain;
output.Attributes.SetAttribute("href", "mailto:" + target);
output.Content.SetContent(target);
}
}

说明:

  • 这个版本使用异步的 ProcessAsync 方法。异步的 GetChildContentAsync 返回 Task ,其包含了TagHelperContent
  • 我们使用 output 参数取得 HTML 元素内容。

2.对 Views/Home/Contact.cshtml 文件做以下更改使 tag helper 取得目标 email。

@{
ViewData["Title"] = "Contact";
}
<h2>@ViewData["Title"].</h2>
<h3>@ViewData["Message"]</h3> <address>
One Microsoft Way<br />
Redmond, WA 98052<br />
<abbr title="Phone">P:</abbr>
425.555.0100
</address> <address>
<strong>Support:</strong><email>Support</email><br />
<strong>Marketing:</strong><email>Marketing</email>
</address>

3.运行应用并验证生成了有效的 email 链接。

粗体(Bold) Tag helper

1.添加以下 BoldTagHelper 类到 TagHelpers 文件夹。

using Microsoft.AspNetCore.Razor.TagHelpers;

namespace AuthoringTagHelpers.TagHelpers
{
[HtmlTargetElement(Attributes = "bold")]
public class BoldTagHelper : TagHelper
{
public override void Process(TagHelperContext context, TagHelperOutput output)
{
output.Attributes.RemoveAll("bold");
output.PreContent.SetHtmlContent("<strong>");
output.PostContent.SetHtmlContent("</strong>");
}
}
} /*
* public IActionResult About()
{
ViewData["Message"] = "Your application description page.";
return View("AboutBoldOnly");
// return View();
}
*/

说明:

  • [HtmlTargetElement] 属性传递一个属性参数,指定为任何 HTML 元素包含名为 “bold” 的 HTML 属性,并且类中重写的 Process 方法将被执行。在我们的示例中, Process 方法删除了 “bold” 属性且以 <strong></strong> 标记包含其中内容。
  • 因为我们不想替换已有标签内容,我们必须用 PreContent.SetHtmlContent 方法写 <strong> 开始标签并用 PostContent.SetHtmlContent 方法写 </strong> 闭合标签。

2.修改 About.cshtml 视图,添加一个 bold 属性值。完整代码如下。

@{
ViewData["Title"] = "About";
}
<h2>@ViewData["Title"].</h2>
<h3>@ViewData["Message"]</h3> <p bold>Use this area to provide additional information.</p> <bold> Is this bold?</bold>

3.运行程序。你可以用你喜欢的浏览器审查源代码,会发现标记已被如愿改变。

上面 [HtmlTargetElement] 属性只指向具有属性名为 “bold” 的 HTML 标记, <bold> 元素不会被 tag helper 修改。

4.注释掉 [HtmlTargetElement] 属性行,其目标将为 <bold> 标签,也就是 HTML 形式的标记 <bold> 。请记得,默认的名称转换将从匹配类名 BoldTagHelper 变为匹配 <bold> 标签。

5.运行程序可验证 <bold> 标签已被 tag helper 处理了。

对一个类配置多个 [HtmlTargetElement] 特性的结果将是对目标作逻辑或判断。例如,使用下列代码,bold 标签或 bold 属性将被匹配。

[HtmlTargetElement("bold")]
[HtmlTargetElement(Attributes = "bold")]

当在同一个声明中使用多个属性时,运行时将视为逻辑与关系。例如,使用如下代码,HTML 元素必须命名为 “bold” 并具有 “bold” 属性方能匹配。

[HtmlTargetElement("bold", Attributes = "bold")]

你同样可以使用 [HtmlTargetElement] 来改变目标元素名称。例如,如果你想要使 BoldTagHelper 指向目标 <MyBold> 标签,你应该使用以下属性:

[HtmlTargetElement("MyBold")]

网站信息 Tag Helper

1.添加一个 Models 文件夹。

2.添加下面的 WebsiteContext 类到 Models 文件夹:

using System;

namespace AuthoringTagHelpers.Models
{
public class WebsiteContext
{
public Version Version { get; set; }
public int CopyrightYear { get; set; }
public bool Approved { get; set; }
public int TagsToShow { get; set; }
}
}

3.添加下面的 WebsiteInformationTagHelper 类到 TagHelpers 文件夹。

using System;
using AuthoringTagHelpers.Models;
using Microsoft.AspNetCore.Razor.TagHelpers; namespace AuthoringTagHelpers.TagHelpers
{
public class WebsiteInformationTagHelper : TagHelper
{
public WebsiteContext Info { get; set; } public override void Process(TagHelperContext context, TagHelperOutput output)
{
output.TagName = "section";
output.Content.SetHtmlContent(
$@"<ul><li><strong>Version:</strong> {Info.Version}</li>
<li><strong>Copyright Year:</strong> {Info.CopyrightYear}</li>
<li><strong>Approved:</strong> {Info.Approved}</li>
<li><strong>Number of tags to show:</strong> {Info.TagsToShow}</li></ul>");
output.TagMode = TagMode.StartTagAndEndTag;
}
}
}

说明:

  • 如前文所述,tag helper 将 tag helper 的 C# 类名和属性 Pascal 形式转换为 小写 kebab 形式。尽管如此,在 Razor 中使用 WebsiteInformationTagHelper 你将能输出 <website-information />
  • 我们并非明确要使用 [HtmlTargetElement] 属性指定目标元素,因此, website-information 的默认方式将被作为目标。如果你使用下面的属性(注意它不是 kebab 形式而是匹配类名):
[HtmlTargetElement("WebsiteInformation")]

小写的 kebab 标签 <website-information /> 不会被匹配。如果你要使用 [HtmlTargetElement] 属性,你应该使用如下所示的 kebab 形式:

[HtmlTargetElement("Website-Information")]
  • 自闭合元素没有内容。在这个例子,Razor 标记将使用自闭合标签,但 tag helper 将创建一个section 元素(是指非闭合的并且我们在 section 元素内部输出内容的元素)。因此,我们需要设置 TagModeStartTagAndEndTag 来输出。换言之,你可以注释掉 TagMode 设置行,并用闭合标签书写标记。(示例标记在本教程下文中提供)
  • 下面代码行中的 $ (美元符号) 使用 interpolated string
$@"<ul><li><strong>Version:</strong> {Info.Version}</li>

5.在 About.cshtml 视图添加下列标记。高亮的标记显示了网站信息。

@using AuthoringTagHelpers.Models
@{
ViewData["Title"] = "About";
}
<h2>@ViewData["Title"].</h2>
<h3>@ViewData["Message"]</h3> <p bold>Use this area to provide additional information.</p> <bold> Is this bold?</bold> <h3> web site info </h3>
<website-information info="new WebsiteContext {
Version = new Version(1, 3),
CopyrightYear = 1638,
Approved = true,
TagsToShow = 131 }" />

说明: 在 Razor 标记中如下:

<website-information info="new WebsiteContext {
Version = new Version(1, 3),
CopyrightYear = 1638,
Approved = true,
TagsToShow = 131 }" />

Razor 知道 info 属性是一个类名,不是字符串,你需要写 C# 代码。一些非字符 tag helper 属性不应该写 @ 字符。

6.运行应用,导航到关于视图查看网站信息。

说明:

  • 你可以使用下面的有闭标签的标记,并移除 tag helper 中有 TagMode.StartTagAndEndTag 的代码行:
<website-information info="new WebsiteContext {
Version = new Version(1, 3),
CopyrightYear = 1638,
Approved = true,
TagsToShow = 131 }" >
</website-information>

条件 Tag Helper

条件 tag helper 在传值为真的时候渲染输出。

1.添加下面的 ConditionTagHelper 类到 TagHelpers 文件夹。

using Microsoft.AspNetCore.Razor.TagHelpers;

namespace AuthoringTagHelpers.TagHelpers
{
[HtmlTargetElement(Attributes = nameof(Condition))]
public class ConditionTagHelper : TagHelper
{
public bool Condition { get; set; } public override void Process(TagHelperContext context, TagHelperOutput output)
{
if (!Condition)
{
output.SuppressOutput();
}
}
}
}

2.使用下面的标记替换 Views/Home/Index.cshtml 文件中的内容:

@using AuthoringTagHelpers.Models
@model WebsiteContext @{
ViewData["Title"] = "Home Page";
} <div>
<h3>Information about our website (outdated):</h3>
<Website-InforMation info=Model />
<div condition="Model.Approved">
<p>
This website has <strong surround="em"> @Model.Approved </strong> been approved yet.
Visit www.contoso.com for more information.
</p>
</div>
</div>

3.用下面的代码替换 Home 控制器中的 Index 方法:

  public IActionResult Index(bool approved = false)
{
return View(new WebsiteContext
{
Approved = approved,
CopyrightYear = 2015,
Version = new Version(1, 3, 3, 7),
TagsToShow = 20
});
}

4.运行应用打开首页。在有条件的 div 中的标记不会被渲染。在URL请求字符串后添加 ?approved=true (例如: http://localhost:1235/Home/Index?approved=true)。approved 被设置 true,有条件的标记将被显示。

说明: 我们使用 nameof 运算符来把属性识别为目标,而非像我们用 bold tag helper 所做的指定字符串。

[HtmlTargetElement(Attributes = nameof(Condition))]
// [HtmlTargetElement(Attributes = "condition")]
public class ConditionTagHelper : TagHelper
{
public bool Condition { get; set; } public override void Process(TagHelperContext context, TagHelperOutput output)
{
if (!Condition)
{
output.SuppressOutput();
}
}
}

nameof 运算符可以在代码被重构的时候保护代码(我们可能想将名称改为 RedCondition)。

避免 Tag Helper 冲突

在这一节,我们将写一对自动链接的 tag helper。首先将替换包含以 HTTP 为首的链接的标记为包含相同 URL(从而产生一个指向 URL 的链接)的 HTML 锚标签。其次将对以 www 为首的 URL 做同样的操作。

因为这两个 Helper 密切相关,我们未来将会重构它们,我们将它们放在同一文件。

1.添加下面的 AutoLinker 类到 TagHelpers 文件夹。

[HtmlTargetElement("p")]
public class AutoLinkerHttpTagHelper : TagHelper
{
public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
var childContent = await output.GetChildContentAsync();
// Find Urls in the content and replace them with their anchor tag equivalent.
output.Content.SetHtmlContent(Regex.Replace(
childContent.GetContent(),
@"\b(?:https?://)(\S+)\b",
"<a target=\"_blank\" href=\"$0\">$0</a>")); // http link version}
}
}

说明: AutoLinkerHttpTagHelper 类指向 p 元素且使用 正则 来创建锚。

2.添加下面的标记到 Views/Home/Contact.cshtml 文件末尾:

@{
ViewData["Title"] = "Contact";
}
<h2>@ViewData["Title"].</h2>
<h3>@ViewData["Message"]</h3> <address>
One Microsoft Way<br />
Redmond, WA 98052<br />
<abbr title="Phone">P:</abbr>
425.555.0100
</address> <address>
<strong>Support:</strong><email>Support</email><br />
<strong>Marketing:</strong><email>Marketing</email>
</address> <p>Visit us at http://docs.asp.net or at www.microsoft.com</p>

3.运行程序并验证 tag helper 正确渲染了锚链接。

4.更新 AutoLinker 类,添加 AutoLinkerWwwTagHelper ,它将转换 www 文字为同样包含原始 www 文字的链接标签。修改的代码是下面高亮部分:

[HtmlTargetElement("p")]
public class AutoLinkerHttpTagHelper : TagHelper
{
public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
var childContent = await output.GetChildContentAsync();
// Find Urls in the content and replace them with their anchor tag equivalent.
output.Content.SetHtmlContent(Regex.Replace(
childContent.GetContent(),
@"\b(?:https?://)(\S+)\b",
"<a target=\"_blank\" href=\"$0\">$0</a>")); // http link version}
}
} [HtmlTargetElement("p")]
public class AutoLinkerWwwTagHelper : TagHelper
{
public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
var childContent = await output.GetChildContentAsync();
// Find Urls in the content and replace them with their anchor tag equivalent.
output.Content.SetHtmlContent(Regex.Replace(
childContent.GetContent(),
@"\b(www\.)(\S+)\b",
"<a target=\"_blank\" href=\"http://$0\">$0</a>")); // www version
}
}

5.运行应用。注意 www 文字被渲染为一条链接,但 HTTP 文字却没有。如果你在两个类中打断点,你可以发现 HTTP tag helper 类先运行。在稍后的教程中我们将看到如何控制其中 tag helper 执行顺序。问题在于 tag helper 输出是被缓存的,而当 WWW tag helper 在运行的时候,它覆盖了来自 HTTP tag helper 的输出缓存。我们将使用下面的代码来修复它:

    public class AutoLinkerHttpTagHelper : TagHelper
{
public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
var childContent = output.Content.IsModified ? output.Content.GetContent() :
(await output.GetChildContentAsync()).GetContent(); // Find Urls in the content and replace them with their anchor tag equivalent.
output.Content.SetHtmlContent(Regex.Replace(
childContent,
@"\b(?:https?://)(\S+)\b",
"<a target=\"_blank\" href=\"$0\">$0</a>")); // http link version}
}
} [HtmlTargetElement("p")]
public class AutoLinkerWwwTagHelper : TagHelper
{
public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
var childContent = output.Content.IsModified ? output.Content.GetContent() :
(await output.GetChildContentAsync()).GetContent(); // Find Urls in the content and replace them with their anchor tag equivalent.
output.Content.SetHtmlContent(Regex.Replace(
childContent,
@"\b(www\.)(\S+)\b",
"<a target=\"_blank\" href=\"http://$0\">$0</a>")); // www version
}
}
}

说明: 在第一个 auto-linking tag helper 版本中,我们使用下面的代码取得目标的内容:

var childContent = await output.GetChildContentAsync();

也就是,我们使用 TagHelperOutput 调用 GetChildContentAsync 传入了 ProcessAsync 方法。如前面提到的,因为输出是缓存的,最终运行的 tag helper 成功。我们使用下面的代码来修复这个问题:

var childContent = output.Content.IsModified ? output.Content.GetContent() :
(await output.GetChildContentAsync()).GetContent();

上面的代码检查可见内容是否已被改变,如果已经存在,则从输出缓冲中获取内容。

7.运行应用可验证两个链接如愿执行。在表现出我们的自动链接 tag helper 是完全正确的同时,它还有个小问题。如果 www tag helper 首先运行,www 链接不正常了。添加 Order 重载修改代码来控制其中 tag 的运行的顺序。Order 属性决定指向同一目标元素的相关 tag helper 的执行顺序。顺序默认值为 0 ,越小的值被优先执行。

public class AutoLinkerHttpTagHelper : TagHelper
{
// This filter must run before the AutoLinkerWwwTagHelper as it searches and replaces http and
// the AutoLinkerWwwTagHelper adds http to the markup.
public override int Order
{
get { return int.MinValue; }
}

以上代码将授权 HTTP tag helper 在 WWW tag helper 之前执行。将 Order 改为 最大值 可验证为 WWW 标签生成的标记不正确。

审查并检索子集内容

tag-helper 提供了多种属性来检索内容。

  • GetChildContentAsync 的结果可被附加到 output.Content
  • 你可以使用 GetContent 审查 GetChildContentAsync 的结果。
  • 如果你修改 output.Content,TagHelper 内容将不被执行或渲染,除非你像在我们的 auto-linker 示例中调用 GetChildContentAsync
public class AutoLinkerHttpTagHelper : TagHelper
{
public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
var childContent = output.Content.IsModified ? output.Content.GetContent() :
(await output.GetChildContentAsync()).GetContent(); // Find Urls in the content and replace them with their anchor tag equivalent.
output.Content.SetHtmlContent(Regex.Replace(
childContent,
@"\b(?:https?://)(\S+)\b",
"<a target=\"_blank\" href=\"$0\">$0</a>")); // http link version}
}
}
  • 多次调用 GetChildContentAsync 将返回相同的值,而不是重复执行 TagHelper 主体,除非你传入一个 false 参数指示不使用缓存结果。

返回目录