《ASP.NET MVC 5 破境之道》:第一境 ASP.Net MVC5项目初探 — 第三节:View层简单改造

第一境 ASP.Net MVC5项目初探 — 第三节:View层简单改造

看到这里,您一定已经迫不及待的想要动手了。下面,我们通过一些对视图的修改,来深入了解Razor能给我们带来什么惊喜。

一、修改页面标题

有过HTML开发经验的读者,都知道页面标题,是在HTML页面的<title>...</title>标签中指定的。

默认情况下,MVC视图的标题为[xxx] - My ASP.NET Application,我们想把“ - My ASP.NET Application”替换为“ - Honor Shop”。

查找顺序是由内而外,因为内层配置会覆盖外层,所以我们先看Home目录中的视图文件,有没有指定标题。默认是没有的。

接下来看Home目录中,有没有建立新的_ViewStart.cshtml,因为它也可以覆盖全局配置。默认也是没有的。

接下来再找上级目录,也就是/Views目录中的_ViewStart.cshtml,默认是可以找到的,这个文件里默认指定的Layout是"~/Views/Shared/_Layout.cshtml",其中“~/”代表虚拟根目录,是相对路径的表示方法,它指向了/Views/Shared/_Layout.cshtml文件。

打开_Layout.cshtml文件,我们可以看到<title>@ViewBag.Title - My ASP.NET Application</title>,可能现在还不太明白@ViewBag.Title是什么意思,不过先不用管它,总之,格式看起来很像“[xxx] - My ASP.NET Application”。

我们把<title>@ViewBag.Title - My ASP.NET Application</title>修改为<title>@ViewBag.Title - Honor Shop</title>。

运行一下,可以看到标题栏已经变成“[xxx] - Honor Shop”的格式了。

接下来,我们来看看[xxx]是从哪儿来的。还是按照刚才的顺序,这次比较幸运,在控制器视图文件里就发现了一些端倪。每个视图文件的开头都有如此一段代码:

@{ ViewBag.Title = "xxx"; }

有的同学会疑惑,这是个什么鬼,HTML里面,没见过啊。这里,结合刚才我们用到的<title>@ViewBag.Title - My ASP.NET Application</title>一起来说明一下,首先,我们使用的是Razor视图引擎,'@'符号,在Razor中有特殊的含义,它标明一段服务器端代码的开始。

C# 的主要 Razor 语法规则

Razor 代码封装于 @{ ... } 中

行内表达式(变量和函数)以 @ 开头

代码语句以分号结尾

字符串由引号包围

C# 代码对大小写敏感

C# 文件的扩展名是 .cshtml

C# 实例

<!-- 单行代码块 -->
@{    var myMessage =    "Hello World"; }
​
<!-- 行内表达式或变量 -->
<p>The value of myMessage is: @myMessage</p><!-- 多行语句代码块 -->
@{
    var greeting = "Welcome to our site!";
    var weekDay = DateTime.Now.DayOfWeek;
    var greetingMessage = greeting + " Here in Huston it is: " + weekDay;
}
<p>The greeting is: @greetingMessage</p>

至于ViewBag.Title = "xxx";,代码中并没有ViewBag的定义,首先想到它会不会是Razor内置的东东,要么,就是MVC提供的东东,但要解释清楚它,目前还有点复杂,这里讲解会牵扯的东西比较多,为了保持对视图的改造的流畅性,我们后面再介绍它。但现在可以猜测的出它是一个服务器端对象,并且它有一个Title属性。虽然这个猜测有点偏激,姑且先这么理解它吧。

那么,书归正传,就尝试替换一下吧,看看效果,正如所料,[xxx],被替换掉了。

fdb5e9744b0e46afb452ce6b609897ed

也有细心的同学,会发现我的标题栏里的图标怎么于自己的不一样,下面我们就来更改标题栏的图标吧。

二、修改标题栏图标

学习过HTML的读者,都知道这是一个比较基础知识,我们不难发现,在项目的根目录下,“躺着”一个名叫favicon.ico的文件。对了,就是它,图标文件的扩展名为.ico,读者可以从网上随意下载一个ico文件,来替换它。我为Honor Shop精心设计了一个,简洁又富有科技感的图标:)自我陶醉一下。

honorshop_logo

也可以通过在html的<head>...</head>标签中加入如下代码来改变favicon的路径及名称。

<head>
    <title>@ViewBag.Title - Honor Shop</title>
    <link rel="icon" href="~/Content/Images/honorshop.ico" type="image/x-icon" />
    <link rel="shortcut icon" href="~/Content/Images/honorshop.ico" type="image/x-icon" />
</head>

三、修改页脚

先修改页脚,不为别的,代码少,改起来简单:P,在页面底部,很轻松就找到了:

<footer>
    <p>© @DateTime.Now.Year - My ASP.NET Application</p>
</footer>

简单修改为:

<footer>
    <p>© @DateTime.Now.AddYears(-3).Year - Honor Shop</p>
</footer>

运行看看,效果实现,也简单使用了一下在Razor中进行服务端编码的快感。

 

不过这时发现,好像很多地方都用到了“Honor Shop”这段字符串,这是我的网站的名字,可以预料,之后,还会有很多地方会用到。当然,方法有很多了,比如写入配置文件,比如写入数据库,等等。这里为了折腾,暂时不考虑这些方法,既然我们现在是对视图的修改,本着学习的目的,先在视图层面想办法,比如在布局视图中加一个变量,然后,视图里就可以引用这个变量了,也比四处硬编码来得爽快。

我们现在_Layout.cshtml的顶端加入如下代码:

@{ var SiteName = "My Honor Shop"; }

接着,我们对页面标题进行更新:

<title>@ViewBag.Title - @SiteName</title>

再更新页脚

<p>© @DateTime.Now.AddYears(-3).Year - @SiteName</p>

运行一下看看,哎哟,不错哟。

不过,这里有个问题,如果我们的项目中,使用了多个布局文件,那不是要在每个布局文件中都定义一遍SiteName?而且,经过试验,在控制器视图中,也没有办法直接使用@SiteName,因为控制器视图与_Layout视图并没有继承关系。

这里提出第一个方案,还记得_ViewStart先于其他视图运行,它的代码里只是指定了一个Layout属性,别的什么都没做。但是这个Layout却可以在控制器视图中进行重写覆盖,由此灵感而发,这个Layout属性到底是谁的属性?在Layout上点击鼠标右键选择[Go To Definition]项或者使用快捷键[F12]跳转到Layout的声明处。

0158084ae29b4bcca9c94bef1a2dbfbb

可以看到,Layout是一个抽象类StartPage的属性,既然这个类是一个抽象类,那么它就不能被实例化,但不管是哪个类继承自它,也就同样继承Layout属性,既然Layout能够在其他视图中使用,那么与Layout平起平坐的其他属性,肯定也可以。所以,第一眼就瞄到了PageData这个属性,它是一个字典,Key是object类型,Value是dynamic类型,看起来,都挺合适的。于是乎,在_ViewStart.cshtml中做些手脚:

@{
    /* 在PageData中添加站点名称键值对 */
    PageData.Add("SiteName", "My First Solution 4 Page Title - Honor Shop");
​
    Layout = "~/Views/Shared/_Layout.cshtml";
}

以页面标题为例,对视图中所有使用@SiteName的地方进行更新:

<title>@ViewBag.Title - @PageData["SiteName"]</title>

运行一下,效果如同期望一样,在布局视图和控制器视图中,都可以使用:

clipboard

第二个方案就是我们之前还很朦胧的ViewBag,与查看Layout属性的方法一样,故技重施,跳转到ViewBag的声明处。

93454756b349460eb7eea3ba795af0cb

可以看到,ViewBag是一个抽象类WebViewPage的属性,而且是动态属性,那么如法炮制,再对_ViewStart.cshtml中做些手脚:

clipboard

但是很遗憾,在_ViewStart中不能操作ViewBag属性。这是为什么呢?再仔细看看上面两个抽象类,原来他们分别在不同的命名空间,也就是说,_ViewStart和控制器视图,处于不同命名空间。第二个方案宣告失败,放弃,就是这么随性:D

不过于此同时,我们还是有所收获的,在返回头来看看第二个抽象类WebViewPage,尤其是它内部声明的一系列属性,我可以很负责任的告诉你,你的整个.NET MVC生涯都是在与它们打交道,不信?你等着瞧……

关于页脚的修改,暂时告一段落,下面来修改导航栏吧。

四、修改导航栏

知己知彼方能百战不殆,我们先看看导航栏原来长得什么样子。

<div class="navbar navbar-inverse navbar-fixed-top">
    <div class="container">
        <div class="navbar-header">
            <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
                <span class="icon-bar"></span>
                <span class="icon-bar"></span>
                <span class="icon-bar"></span>
            </button>
            @Html.ActionLink("Application name", "Index", "Home", new { area = "" }, new { @class = "navbar-brand" })
        </div>
        <div class="navbar-collapse collapse">
            <ul class="nav navbar-nav">
                <li>@Html.ActionLink("Home", "Index", "Home")</li>
                <li>@Html.ActionLink("About", "About", "Home")</li>
                <li>@Html.ActionLink("Contact", "Contact", "Home")</li>
            </ul>
        </div>
    </div>
</div>

忽略掉繁杂的HTML标签和各种样式,标重点,第9、13、14、15行。第13~15行很相似,只是参数的内容不同,第9行与其它几行,都是以@Html.ActionLink开头,参数个数不同。看来都是使用了Html的ActionLink的重载方法。

等等,'@'符我们说过了,表明后面要接服务端代码了,那么,Html又是什么鬼?我们还是跳转到声明处看看吧

0bb98e91908544018c662b252299592f

首先,它是一个抽象类WebViewPage<TModel>的属性,WebViewPage<TModel>类又继承自WebViewPage类,WebViewPage类就是刚才我们看到的声明了ViewBag的类,兜兜转转啊,不过,WebViewPage类中也声明了一个Html,只不过是HtmlHelper<object>类型,这里的Html是HtmlHelper<TModel>类型,其实它们两个都是同根同源的,都是HtmlHelper<TModel>类的实例。同时,也可以看出,两个WebViewPage,一个是强类型的,一个是弱类型的。继续追踪HtmlHelper<TModel>类,它继承自HtmlHelper类,具有两个构造函数,还有两个只读属性,没有方法声明,那么,方法应该是都在HtmlHelper基类中声明的了,跟进去一看,大跌眼镜,虽然有声明了一堆方法,但是居然没有我们期待已久的ActionLink方法,此刻,必须要想到扩展方法,如果没有想到,那么请恶补C#的相关细节。返回到_Layout.cshtml,直接跳转到ActionLink方法的声明,眼前一亮,原来都在这里:

e32f5ac0bc2f49209fee3852b8f8321a

大家可以展开类及方法上的Summary信息来了解一些信息。从类名上可以看出,这是一个针对链接的扩展类。从方法名可以看出,链接可以分为两种,一种是ActionLink,另一种是RouteLink。RouteLink可以简单理解为通过路由(不是通常所说的路由器阿)跳转,用到时再详细介绍。我们先来看ActionLink。

ActionLink有10个重载,每个都讲,也是比较辛苦的,毕竟我也很懒……我们就拿导航栏中第9行代码使用的重载来做一个说明吧。

//
// 根据指定的链接文字,操作名称,控制器名称,路由参数对象和html属性对象,
// 返回一个锚点元素(也就是html中的a标签),
// MvcHtmlString是一个特殊的字符串,是经过Html-encoding的html字符串,这点很重要。
//
public static MvcHtmlString ActionLink(this HtmlHelper htmlHelper
    // 锚点标签中显示的文字
    , string linkText
    // 控制器中操作的名称
    , string actionName
    // 控制器名称
    , string controllerName
    // 包含路由参数的对象,通过反射检测routeValues对象的属性获取路由参数。
    // 通常使用对象初始化器语法创建routeValues对象。
    , object routeValues
    // 一个包含html属性列表的对象
    , object htmlAttributes);

1546035326(1)

目前,我们也只能是修改第一个参数,动手:

<div class="navbar navbar-inverse navbar-fixed-top">
    <div class="container">
        <div class="navbar-header">
            <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
                <span class="icon-bar"></span>
                <span class="icon-bar"></span>
                <span class="icon-bar"></span>
            </button>
            @Html.ActionLink(@PageData["SiteName"] as string // @PageData["SiteName"]的值是dynamic类型
                            , "Index", "Home", new { area = "" }, new { @class = "navbar-brand" })
        </div>
        <div class="navbar-collapse collapse">
            <ul class="nav navbar-nav">
                <li>@Html.ActionLink("首页", "Index", "Home")</li>
                <li>@Html.ActionLink("关于我们", "About", "Home")</li>
                <li>@Html.ActionLink("联系我们", "Contact", "Home")</li>
            </ul>
        </div>
    </div>
</div>

92abba08fccc4bb8bdd85f644e65651b

这样就完成了对导航信息的基本修改。

现在我们可以初步体会到,@Html.ActionLink可以帮助我们生成一个锚点元素,它是.NET MVC视图引擎提供的一个辅助方法。

Html就是对HtmlHelper的封装,它能帮助我们生成页面上所需要的各种元素。

但我现在又想把我辛辛苦苦设计的Logo放上去,替换干巴巴的“Honor Shop"文字。

五、添加带链接的图片

首先,将制作好的Logo文件(logo.png)拷贝到/Content/Images/下。将原来的锚点代码注释掉。

@*@Html.ActionLink(@PageData["SiteName"] as string // @PageData["SiteName"]的值是dynamic类型
                   , "Index", "Home", new { area = "" }, new { @class = "navbar-brand" })*@

Razor中使用@*....*@来注释,看起来,还挺有喜感的。

接下来,我们先立个目标,最终要生成一个什么样的html,能够满足我们添加的Logo的需求。

<a href="/Home/Index" class="navbar-brand" style="padding-top: 0px; padding-bottom:0px;">
    <img src="/Content/Images/logo.png" alt="Honor Shop" title="Honor Shop" style="height: 100%; background-color:white;">
</a>

这里没有什么特别的,都是一些Html和CSS的基础的东西,不是本书的重点,不多介绍,看看效果。

07720172f9174d5c86f860b9bd6a5157

目前,它可以很好的工作,但它是脆弱的。思考一个问题,如果,我们的应用,并没有部署在网站的根目录,或者修改了路由的定义,那么,锚点的href和图片的src的值,都有可能把浏览器导航到一个网站上并不存在的资源处。再做一个假设,我们的应用里有很多这样的锚点加图片的元素,他们在编译时,并不会报告错误,应用将变得非常难以维护。

更好的办法就是可以通过路由来计算路径,这样就可以有效的解决上面提出的部署位置和修改路由的问题了。

.NET Web应用为我们提供了一套路由机制,可以为我们计算路由路径,其核心就是RouteTable,我们可以通过

RouteTable.Routes.GetVirtualPath(RequestContext requestContext, RouteValueDictionary values).VirtualPath;
RouteTable.Routes.GetVirtualPath(RequestContext requestContext, string routeName, RouteValueDictionary values).VirtualPath;

这两个重载方法来计算路径,是不是很开心,那么来看看参数列表,我们是不是都具备:

第一个参数,requestContext,不用操心,视图引擎已经为我们提供了,可以通过this.ViewContext.RequestContext来获取;

第二个参数,values,他的类型是RouteValueDictionary,是一个字典,我们可以通过这个字典,提交路由所需的参数;

第三个参数,routeName,可以用来指定我们需要使用哪条路由;

路由是MVC的一个重要机制,更是ASP.NET核心框架的一部分,还记得我们在本境第二节中介绍App_Start目录时,提及到RouteConfig,这个类就是用来管理配置路由的,并且在应用启动时,Global中的Application_Start方法中会调用它的RegisterRoutes方法来注册路由。

ASP.NET MVC框架中的路由主要有两个用途:

1. 匹配传入的请求(该请求不匹配服务器文件系统中的文件或资源),并把这些请求映射到控制器操作。

2. 构造传出的URL,用来响应控制器操作。

现在我们打开RouteConfig,看看默认提供的路由是什么样的。

using System.Web.Mvc;
using System.Web.Routing;
​
namespace HonorShop.Web
{
    public class RouteConfig
    {
        public static void RegisterRoutes(RouteCollection routes)
        {
            routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
​
            // 注册一条路由配置
            routes.MapRoute(
                // 定义路由的名称,名称可以自定义,但在路由表中不可重复。
                name: "Default",
                // 定义一条url访问的模式
                url: "{controller}/{action}/{id}",
                // 指定路由的默认值
                defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
            );
        }
    }
}

通过上面的代码,可以看出,注册一条路由所需要的参数,以及他们的含义。

对于构造首页的url,我们知道controller是Home,action是Index,id是可选的,可以不提供。

但是对于图片的url,就没有那么幸运了,因为图片是静态资源,并没有明确的controller和action。很显然,默认提供的路由,不适合我们。不如,我们动手来添加一条路由吧:

// 注册一条路由指向图片资源
routes.MapRoute(
    // 定义本条路由的名称为Images
    name: "Images",
    // 定义url匹配的模式
    url: "Content/Images/{imgName}",
    // 定义本条路由的默认参数值,这里配置为可选;
    // 其实更好的建议是配置为一张“图片未找到”“图片已损坏”等含义的图片名称;
    defaults: new { imgName = UrlParameter.Optional }
);

好了,准备工作都已经就绪了,让我们返回_Layout.cshtml,在它顶部开始撸代码:

@{
    var context = this.ViewContext.RequestContext;
    var href_values = new RouteValueDictionary { { "controller", "home" }, { "action", "index" } };
    var href = RouteTable.Routes.GetVirtualPath(context, href_values).VirtualPath;
    var src_values = new RouteValueDictionary { { "id", "logo.png" } };
    var src = RouteTable.Routes.GetVirtualPath(context, "Images", src_values).VirtualPath;
}

方法都很简单,前面也对细节都解释过了,下面接着修改我们的锚点和图片元素:

<a href="@href" class="navbar-brand" style="padding-top: 0px; padding-bottom:0px;">
    <img src="@src" alt="Honor Shop" title="Honor Shop" style="height: 100%; background-color:white;" />
</a>

运行效果与图14一样,完美。完美是完美,不过实现的过程,未免复杂了些,而且这里只是解决了路径计算的问题,如果有很多路径需要计算,参数也是各种各样,可以想象计算路径给我们带来的如此的繁重的代码量,对于偷奸耍滑成性的MVC团队来说,这也太不能忍了。

值得庆幸的是,MVC给我们提供了很多与链接相关的辅助方法:

    • Html.ActionLink:我们前面刚介绍过;
    • Url.Action:根据给定的Controller,Action 生成链接,但是Html.ActionLink返回的是MvcHtmlString的一个带<a>标签的超链接,而Url.Action返回的是string,一个根据Controller,Action生成的URL地址,比Html.ActionLink少了<a>标签;
    • Html.RouteLink 与 Url.RouteUrl:两者都是可以指定由哪一个路由来生成Url,其它与上面的ActionLInk,Action一样;
    • Url.Content:将虚拟(相对)路径转换为应用程序绝对路径。

这次,我们来使用两个Url属性提供的方法,毕竟我们是用来计算路径,Url看起来比Html更贴切一些。将之前直接使用路由所做的更改,全都删除或者注释掉,对的,就是这么随性:

<a href="@Url.Action("Index", "Home")" class="navbar-brand" style="padding-top: 0px; padding-bottom:0px;">
    <img src="@Url.Content("~/Content/Images/logo.png")"
    alt="Honor Shop" title="Honor Shop" style="height: 100%; background-color:white;" />
</a>

运行看看:

使用辅助方法计算资源路径效果

没有意外,一切按计划行事。但为了早日冲入第二境,我决定再用@Html提供的辅助方法折腾一遍,但是悲剧的是在@Html中并没有找到与图片相关的扩展方法。这就有点悲剧了。

如果只用@Html.ActionLink能不能实现呢,能,可以把Logo图片加到样式表的背景图里,ActionLink的htmlAttributes应用样式,也是可以的。也比较简单,但对SEO不够友好,特殊场景还是可以使用的,这里就不实操了。

不过这时,想起了之前提到的@Html.ActionLink是一系列扩展的重载方法,这些方法都可以辅助生成锚点元素。那么,我们是不是也可以通过扩展方法,来生成符合我们要求的自定义图片链接元素呢?说干就干。

在项目中创建一个Html目录,用来放置对HtmlHelper扩展方法的类文件;

在Html目录中新建类LinkExtensions,修改为静态类(必须,可以参考C#扩展方法的实现);

using System.Collections.Generic;
using System.Web.Mvc;
using System.Web.Routing;
​
namespace HonorShop.Web.Html
{
    public static class LinkExtensions
    {
        /// <summary>
        /// Summary:
        ///     扩展ActionLink方法,用来创建带锚点的图片元素;
        /// </summary>
        /// <param name="htmlHelper">扩展类</param>
        /// <param name="actionName">操作名</param>
        /// <param name="controllerName">控制器名</param>
        /// <param name="routeValues">路由参数列表</param>
        /// <param name="linkAttributes">锚点htmlAttributes</param>
        /// <param name="imagePath">图片路径</param>
        /// <param name="imageAttributes">图片htmlAttributes</param>
        /// <returns>An image element (img element) within an anchor element (a element).</returns>
        public static MvcHtmlString ActionLink(this HtmlHelper htmlHelper
            , string actionName
            , string controllerName
            , RouteValueDictionary routeValues
            , IDictionary<string, string> linkAttributes
            , string imagePath
            , IDictionary<string, string> imageAttributes)
        {
            var urlHelper = new UrlHelper(htmlHelper.ViewContext.RequestContext);
​
            string imgUrl = urlHelper.Content(imagePath);
            TagBuilder imgTagBuilder = new TagBuilder("img");
            imgTagBuilder.MergeAttribute("src", imgUrl);
            if (null != imageAttributes && 0 < imageAttributes.Count)
                imgTagBuilder.MergeAttributes(imageAttributes, true);
            string img = imgTagBuilder.ToString(TagRenderMode.SelfClosing);
​
            string url = urlHelper.Action(actionName, controllerName, routeValues);
​
            TagBuilder tagBuilder = new TagBuilder("a")
            {
                InnerHtml = img
            };
            tagBuilder.MergeAttribute("href", url);
            if (null != linkAttributes && 0 < linkAttributes.Count)
                tagBuilder.MergeAttributes(linkAttributes, true);
​
            return new MvcHtmlString(tagBuilder.ToString(TagRenderMode.Normal));
        }
    }
}

在_Layout.cshtml的顶部添加引用

@using HonorShop.Web.Html;

修改导航栏Logo位置代码

@Html.ActionLink("Index",
        "Home",
        new RouteValueDictionary { { "area", string.Empty } },
        new Dictionary<string, string> {
            { "alt", "Honor Shop" },
            { "title", "Honor Shop" },
            { "class", "navbar-brand" },
            { "style", "padding-top: 0px; padding-bottom:0px;" } },
        "~/Content/Images/logo.png",
        new Dictionary<string, string> {
            { "alt", "Honor Shop" },
            { "title", "Honor Shop" },
            { "style", "height: 100%; background-color:white;" } })

两个字典拼装的有点多,代码显得长了点,不过,一个方法搞定,还是简洁了很多,而且使用了扩展方法,在重用和灵活性上都得到了大幅度的提升。

回想一下,我们在第二部分说过如何修改标题栏图标时,使用了如下代码:

<link rel="icon" href="~/Content/Images/honorshop.ico" type="image/x-icon" />
<link rel="shortcut icon" href="~/Content/Images/honorshop.ico" type="image/x-icon" />

其中,也涉及到了路径问题,那么,我们应用学到的知识,来更新一下它们吧:

<link rel="icon" href="@Url.Content("~/Content/Images/honorshop.ico")" type="image/x-icon" />
<link rel="shortcut icon" href="@Url.Content("~/Content/Images/honorshop.ico")" type="image/x-icon" />

运行一下,效果如图15所示一样。大功告成。

到这里,我们也基本了解了HtmlHelper和UrlHelper两个辅助类的使用方法以及如何为其添加扩展方法。这将在我们后面的开发过程中,打下良好的基础,读者朋友需要细细品味,最好能够跟着动手实际操作一番。

下一节开始,我们就要使用这些知识,动手打造我们的第一个页面了。

 

喜欢本系列丛书的朋友,可以点击链接加入QQ交流群(994761602)【C# 破境之道】
方便各位在有疑问的时候可以及时给我个反馈。同时,也算是给各位志同道合的朋友提供一个交流的平台。
需要源码的童鞋,也可以在群文件中获取最新源代码。

posted @ 2019-01-18 16:32  MikeCheers  阅读(459)  评论(2编辑  收藏  举报