Sử dụng các phần trong mẫu Trình chỉnh sửa / Hiển thị


104

Tôi muốn giữ tất cả mã JavaScript của mình trong một phần; ngay trước bodythẻ đóng trong trang bố cục chính của tôi và chỉ băn khoăn về cách tốt nhất để sử dụng nó, kiểu MVC.

Ví dụ: nếu tôi tạo một DisplayTemplate\DateTime.cshtmltệp sử dụng Bộ chọn ngày tháng của giao diện người dùng jQuery thì tôi sẽ nhúng JavaScript trực tiếp vào mẫu đó nhưng sau đó nó sẽ hiển thị giữa trang.

Ở chế độ xem bình thường, tôi có thể chỉ sử dụng @section JavaScript { //js here }và sau đó @RenderSection("JavaScript", false)trong bố cục chính của mình nhưng điều này dường như không hoạt động trong các mẫu hiển thị / trình chỉnh sửa - bạn có ý kiến ​​gì không?


4
cho bất kỳ ai đến sau - có một gói nuget
Russ Cam

Câu trả lời:


189

Bạn có thể tiến hành với sự kết hợp của hai người trợ giúp:

public static class HtmlExtensions
{
    public static MvcHtmlString Script(this HtmlHelper htmlHelper, Func<object, HelperResult> template)
    {
        htmlHelper.ViewContext.HttpContext.Items["_script_" + Guid.NewGuid()] = template;
        return MvcHtmlString.Empty;
    }

    public static IHtmlString RenderScripts(this HtmlHelper htmlHelper)
    {
        foreach (object key in htmlHelper.ViewContext.HttpContext.Items.Keys)
        {
            if (key.ToString().StartsWith("_script_"))
            {
                var template = htmlHelper.ViewContext.HttpContext.Items[key] as Func<object, HelperResult>;
                if (template != null)
                {
                    htmlHelper.ViewContext.Writer.Write(template(null));
                }
            }
        }
        return MvcHtmlString.Empty;
    }
}

và sau đó trong _Layout.cshtml:

<body>
...
@Html.RenderScripts()
</body>

và ở đâu đó trong một số mẫu:

@Html.Script(
    @<script src="@Url.Content("~/Scripts/jquery-1.4.4.min.js")" type="text/javascript"></script>
)

3
Vì một từ điển không được sắp xếp thứ tự, tôi sẽ làm thế nào trước khi xuất đầu tiên? Thứ tự nó xuất ra là ngẫu nhiên (có lẽ là do Hướng dẫn) ..
eth0

Có lẽ bạn có thể thiết lập một trường số nguyên tĩnh và sử dụng Interlocked.Increment () thay cho GUID để sắp xếp thứ tự, nhưng ngay cả khi đó tôi nghĩ rằng từ điển không bao giờ đảm bảo thứ tự. Theo suy nghĩ thứ hai, có thể một trường tĩnh không phù hợp vì nó có thể bị giữ trên các màn hình trang. Thay vào đó, có thể thêm một số nguyên vào từ điển Mục, nhưng bạn phải đặt một khóa xung quanh nó.
Mark Adamson

Tôi đã bắt đầu sử dụng giải pháp này gần đây, nhưng dường như tôi không thể nhét hai tập lệnh vào một dòng @ Html.Script (), vì tôi không chắc HelperResult hoạt động như thế nào. Không thể thực hiện 2 khối script trong 1 lệnh gọi Html.Script?
Langdon

2
@TimMeers, ý bạn là gì? Đối với tôi, tất cả điều này đã luôn luôn lỗi thời. Tôi sẽ không sử dụng những người trợ giúp đó chút nào. Tôi không bao giờ có nhu cầu bao gồm bất kỳ tập lệnh nào trong các chế độ xem một phần của mình. Tôi chỉ đơn giản là dính vào Razor tiêu chuẩn sections. Trong MVC4 Bundling thực sự có thể được sử dụng cũng như nó giúp giảm kích thước của các tập lệnh.
Darin Dimitrov

4
Cách tiếp cận này không hoạt động nếu bạn muốn đặt các tập lệnh hoặc kiểu của mình trong headthẻ thay vì ở cuối bodythẻ, vì @Html.RenderScripts()sẽ được thực thi trước chế độ xem từng phần của bạn và do đó trước đó @Html.Script().
Maksim Vi.

41

Phiên bản sửa đổi của câu trả lời của Darin để đảm bảo thứ tự. Cũng hoạt động với CSS:

public static IHtmlString Resource(this HtmlHelper HtmlHelper, Func<object, HelperResult> Template, string Type)
{
    if (HtmlHelper.ViewContext.HttpContext.Items[Type] != null) ((List<Func<object, HelperResult>>)HtmlHelper.ViewContext.HttpContext.Items[Type]).Add(Template);
    else HtmlHelper.ViewContext.HttpContext.Items[Type] = new List<Func<object, HelperResult>>() { Template };

    return new HtmlString(String.Empty);
}

public static IHtmlString RenderResources(this HtmlHelper HtmlHelper, string Type)
{
    if (HtmlHelper.ViewContext.HttpContext.Items[Type] != null)
    {
        List<Func<object, HelperResult>> Resources = (List<Func<object, HelperResult>>)HtmlHelper.ViewContext.HttpContext.Items[Type];

        foreach (var Resource in Resources)
        {
            if (Resource != null) HtmlHelper.ViewContext.Writer.Write(Resource(null));
        }
    }

    return new HtmlString(String.Empty);
}

Bạn có thể thêm các tài nguyên JS và CSS như sau:

@Html.Resource(@<script src="@Url.Content("~/Scripts/jquery-1.4.4.min.js")" type="text/javascript"></script>, "js")
@Html.Resource(@<link rel="stylesheet" href="@Url.Content("~/CSS/style.css")" />, "css")

Và hiển thị các tài nguyên JS và CSS như sau:

@Html.RenderResources("js")
@Html.RenderResources("css")

Bạn có thể thực hiện kiểm tra chuỗi để xem liệu nó có bắt đầu bằng tập lệnh / liên kết hay không để bạn không phải xác định rõ ràng từng tài nguyên là gì.


Cảm ơn eth0. Tôi đã thỏa hiệp về vấn đề này, nhưng tôi sẽ phải kiểm tra điều này.
one.beat.consumer

Tôi biết điều này gần 2 năm trước, nhưng có cách nào để kiểm tra xem tệp css / js đã tồn tại và không hiển thị nó không? Cảm ơn
CodingSlayer

1
đồng ý. Không chắc nó có hiệu quả như thế nào, nhưng hiện tại tôi đang làm việc này: var httpTemplates = HtmlHelper.ViewContext.HttpContext.Items [Type] as List <Func <object, HelperResult >>; var prevItem = from q in httpTemplates where q (null) .ToString () == Template (null) .ToString () select q; if (! prevItem.Any ()) {// Thêm mẫu}
CodingSlayer vào

@imAbhi cảm ơn, chỉ là những gì tôi cần, trông giống như một 1-loop của bó với item.ToString vì vậy tôi sẽ nghĩ rằng nó phải đủ nhanh
Kunukn

35

Tôi gặp phải vấn đề tương tự, nhưng các giải pháp được đề xuất ở đây chỉ hoạt động tốt khi thêm tham chiếu vào tài nguyên và không phù hợp lắm với mã JS nội tuyến. Tôi đã tìm thấy một bài viết rất hữu ích và gói tất cả JS nội tuyến (và cả các thẻ script) của tôi trong

@using (Html.BeginScripts())
{
    <script src="@Url.Content("~/Scripts/jquery-ui-1.8.18.min.js")" type="text/javascript"></script>
    <script>
    // my inline scripts here
    <\script>
}

Và trong chế độ xem _Layout được đặt @Html.PageScripts()ngay trước khi đóng thẻ 'body'. Công việc như một cái duyên đối với tôi.


Bản thân những người trợ giúp:

public static class HtmlHelpers
{
    private class ScriptBlock : IDisposable
    {
        private const string scriptsKey = "scripts";
        public static List<string> pageScripts
        {
            get
            {
                if (HttpContext.Current.Items[scriptsKey] == null)
                    HttpContext.Current.Items[scriptsKey] = new List<string>();
                return (List<string>)HttpContext.Current.Items[scriptsKey];
            }
        }

        WebViewPage webPageBase;

        public ScriptBlock(WebViewPage webPageBase)
        {
            this.webPageBase = webPageBase;
            this.webPageBase.OutputStack.Push(new StringWriter());
        }

        public void Dispose()
        {
            pageScripts.Add(((StringWriter)this.webPageBase.OutputStack.Pop()).ToString());
        }
    }

    public static IDisposable BeginScripts(this HtmlHelper helper)
    {
        return new ScriptBlock((WebViewPage)helper.ViewDataContainer);
    }

    public static MvcHtmlString PageScripts(this HtmlHelper helper)
    {
        return MvcHtmlString.Create(string.Join(Environment.NewLine, ScriptBlock.pageScripts.Select(s => s.ToString())));
    }
}

3
đây là câu trả lời tốt nhất; nó cũng cho phép bạn tiêm khá nhiều thứ và trì hoãn nó cho đến khi kết thúc
drzaus 12/09/13

1
Bạn nên sao chép và dán mã từ bài viết trong trường hợp nó bị lỗi! Đây là một câu trả lời xuất sắc!
Shaamaan

Làm thế nào chúng ta có thể làm điều này trong lõi asp.net
ramanmittal Ngày

13

Tôi thích giải pháp được đăng bởi @ john-w-harding, vì vậy tôi kết hợp nó với câu trả lời của @ darin-dimitrov để tạo ra giải pháp có thể quá phức tạp sau đây cho phép bạn trì hoãn việc hiển thị bất kỳ html nào (các tập lệnh cũng vậy) trong một khối đang sử dụng.

SỬ DỤNG

Trong chế độ xem từng phần lặp lại, chỉ bao gồm khối một lần:

@using (Html.Delayed(isOnlyOne: "MYPARTIAL_scripts")) {
    <script>
        someInlineScript();
    </script>
}

Trong chế độ xem một phần (được lặp lại?), Hãy bao gồm khối cho mỗi khi một phần được sử dụng:

@using (Html.Delayed()) {
    <b>show me multiple times, @Model.Whatever</b>
}

Trong chế độ xem từng phần (được lặp lại?), Hãy bao gồm khối một lần và sau đó hiển thị cụ thể theo tên one-time:

@using (Html.Delayed("one-time", isOnlyOne: "one-time")) {
    <b>show me once by name</b>
    <span>@Model.First().Value</span>
}

Để kết xuất:

@Html.RenderDelayed(); // the "default" unidentified blocks
@Html.RenderDelayed("one-time", false); // render the specified block by name, and allow us to render it again in a second call
@Html.RenderDelayed("one-time"); // render the specified block by name
@Html.RenderDelayed("one-time"); // since it was "popped" in the last call, won't render anything

CODE

public static class HtmlRenderExtensions {

    /// <summary>
    /// Delegate script/resource/etc injection until the end of the page
    /// <para>@via https://stackoverflow.com/a/14127332/1037948 and http://jadnb.wordpress.com/2011/02/16/rendering-scripts-from-partial-views-at-the-end-in-mvc/ </para>
    /// </summary>
    private class DelayedInjectionBlock : IDisposable {
        /// <summary>
        /// Unique internal storage key
        /// </summary>
        private const string CACHE_KEY = "DCCF8C78-2E36-4567-B0CF-FE052ACCE309"; // "DelayedInjectionBlocks";

        /// <summary>
        /// Internal storage identifier for remembering unique/isOnlyOne items
        /// </summary>
        private const string UNIQUE_IDENTIFIER_KEY = CACHE_KEY;

        /// <summary>
        /// What to use as internal storage identifier if no identifier provided (since we can't use null as key)
        /// </summary>
        private const string EMPTY_IDENTIFIER = "";

        /// <summary>
        /// Retrieve a context-aware list of cached output delegates from the given helper; uses the helper's context rather than singleton HttpContext.Current.Items
        /// </summary>
        /// <param name="helper">the helper from which we use the context</param>
        /// <param name="identifier">optional unique sub-identifier for a given injection block</param>
        /// <returns>list of delayed-execution callbacks to render internal content</returns>
        public static Queue<string> GetQueue(HtmlHelper helper, string identifier = null) {
            return _GetOrSet(helper, new Queue<string>(), identifier ?? EMPTY_IDENTIFIER);
        }

        /// <summary>
        /// Retrieve a context-aware list of cached output delegates from the given helper; uses the helper's context rather than singleton HttpContext.Current.Items
        /// </summary>
        /// <param name="helper">the helper from which we use the context</param>
        /// <param name="defaultValue">the default value to return if the cached item isn't found or isn't the expected type; can also be used to set with an arbitrary value</param>
        /// <param name="identifier">optional unique sub-identifier for a given injection block</param>
        /// <returns>list of delayed-execution callbacks to render internal content</returns>
        private static T _GetOrSet<T>(HtmlHelper helper, T defaultValue, string identifier = EMPTY_IDENTIFIER) where T : class {
            var storage = GetStorage(helper);

            // return the stored item, or set it if it does not exist
            return (T) (storage.ContainsKey(identifier) ? storage[identifier] : (storage[identifier] = defaultValue));
        }

        /// <summary>
        /// Get the storage, but if it doesn't exist or isn't the expected type, then create a new "bucket"
        /// </summary>
        /// <param name="helper"></param>
        /// <returns></returns>
        public static Dictionary<string, object> GetStorage(HtmlHelper helper) {
            var storage = helper.ViewContext.HttpContext.Items[CACHE_KEY] as Dictionary<string, object>;
            if (storage == null) helper.ViewContext.HttpContext.Items[CACHE_KEY] = (storage = new Dictionary<string, object>());
            return storage;
        }


        private readonly HtmlHelper helper;
        private readonly string identifier;
        private readonly string isOnlyOne;

        /// <summary>
        /// Create a new using block from the given helper (used for trapping appropriate context)
        /// </summary>
        /// <param name="helper">the helper from which we use the context</param>
        /// <param name="identifier">optional unique identifier to specify one or many injection blocks</param>
        /// <param name="isOnlyOne">extra identifier used to ensure that this item is only added once; if provided, content should only appear once in the page (i.e. only the first block called for this identifier is used)</param>
        public DelayedInjectionBlock(HtmlHelper helper, string identifier = null, string isOnlyOne = null) {
            this.helper = helper;

            // start a new writing context
            ((WebViewPage)this.helper.ViewDataContainer).OutputStack.Push(new StringWriter());

            this.identifier = identifier ?? EMPTY_IDENTIFIER;
            this.isOnlyOne = isOnlyOne;
        }

        /// <summary>
        /// Append the internal content to the context's cached list of output delegates
        /// </summary>
        public void Dispose() {
            // render the internal content of the injection block helper
            // make sure to pop from the stack rather than just render from the Writer
            // so it will remove it from regular rendering
            var content = ((WebViewPage)this.helper.ViewDataContainer).OutputStack;
            var renderedContent = content.Count == 0 ? string.Empty : content.Pop().ToString();

            // if we only want one, remove the existing
            var queue = GetQueue(this.helper, this.identifier);

            // get the index of the existing item from the alternate storage
            var existingIdentifiers = _GetOrSet(this.helper, new Dictionary<string, int>(), UNIQUE_IDENTIFIER_KEY);

            // only save the result if this isn't meant to be unique, or
            // if it's supposed to be unique and we haven't encountered this identifier before
            if( null == this.isOnlyOne || !existingIdentifiers.ContainsKey(this.isOnlyOne) ) {
                // remove the new writing context we created for this block
                // and save the output to the queue for later
                queue.Enqueue(renderedContent);

                // only remember this if supposed to
                if(null != this.isOnlyOne) existingIdentifiers[this.isOnlyOne] = queue.Count; // save the index, so we could remove it directly (if we want to use the last instance of the block rather than the first)
            }
        }
    }


    /// <summary>
    /// <para>Start a delayed-execution block of output -- this will be rendered/printed on the next call to <see cref="RenderDelayed"/>.</para>
    /// <para>
    /// <example>
    /// Print once in "default block" (usually rendered at end via <code>@Html.RenderDelayed()</code>).  Code:
    /// <code>
    /// @using (Html.Delayed()) {
    ///     <b>show at later</b>
    ///     <span>@Model.Name</span>
    ///     etc
    /// }
    /// </code>
    /// </example>
    /// </para>
    /// <para>
    /// <example>
    /// Print once (i.e. if within a looped partial), using identified block via <code>@Html.RenderDelayed("one-time")</code>.  Code:
    /// <code>
    /// @using (Html.Delayed("one-time", isOnlyOne: "one-time")) {
    ///     <b>show me once</b>
    ///     <span>@Model.First().Value</span>
    /// }
    /// </code>
    /// </example>
    /// </para>
    /// </summary>
    /// <param name="helper">the helper from which we use the context</param>
    /// <param name="injectionBlockId">optional unique identifier to specify one or many injection blocks</param>
    /// <param name="isOnlyOne">extra identifier used to ensure that this item is only added once; if provided, content should only appear once in the page (i.e. only the first block called for this identifier is used)</param>
    /// <returns>using block to wrap delayed output</returns>
    public static IDisposable Delayed(this HtmlHelper helper, string injectionBlockId = null, string isOnlyOne = null) {
        return new DelayedInjectionBlock(helper, injectionBlockId, isOnlyOne);
    }

    /// <summary>
    /// Render all queued output blocks injected via <see cref="Delayed"/>.
    /// <para>
    /// <example>
    /// Print all delayed blocks using default identifier (i.e. not provided)
    /// <code>
    /// @using (Html.Delayed()) {
    ///     <b>show me later</b>
    ///     <span>@Model.Name</span>
    ///     etc
    /// }
    /// </code>
    /// -- then later --
    /// <code>
    /// @using (Html.Delayed()) {
    ///     <b>more for later</b>
    ///     etc
    /// }
    /// </code>
    /// -- then later --
    /// <code>
    /// @Html.RenderDelayed() // will print both delayed blocks
    /// </code>
    /// </example>
    /// </para>
    /// <para>
    /// <example>
    /// Allow multiple repetitions of rendered blocks, using same <code>@Html.Delayed()...</code> as before.  Code:
    /// <code>
    /// @Html.RenderDelayed(removeAfterRendering: false); /* will print */
    /// @Html.RenderDelayed() /* will print again because not removed before */
    /// </code>
    /// </example>
    /// </para>

    /// </summary>
    /// <param name="helper">the helper from which we use the context</param>
    /// <param name="injectionBlockId">optional unique identifier to specify one or many injection blocks</param>
    /// <param name="removeAfterRendering">only render this once</param>
    /// <returns>rendered output content</returns>
    public static MvcHtmlString RenderDelayed(this HtmlHelper helper, string injectionBlockId = null, bool removeAfterRendering = true) {
        var stack = DelayedInjectionBlock.GetQueue(helper, injectionBlockId);

        if( removeAfterRendering ) {
            var sb = new StringBuilder(
#if DEBUG
                string.Format("<!-- delayed-block: {0} -->", injectionBlockId)
#endif
                );
            // .count faster than .any
            while (stack.Count > 0) {
                sb.AppendLine(stack.Dequeue());
            }
            return MvcHtmlString.Create(sb.ToString());
        } 

        return MvcHtmlString.Create(
#if DEBUG
                string.Format("<!-- delayed-block: {0} -->", injectionBlockId) + 
#endif
            string.Join(Environment.NewLine, stack));
    }


}

Kỳ dị. Tôi không nhớ sao chép câu trả lời cho chủ đề khác này , nhưng tôi đã làm một tốt hơn một chút writeup có ...
drzaus

12

Cài đặt gói nuget Forloop.HtmlHelpers - nó thêm một số trình trợ giúp để quản lý tập lệnh trong các dạng xem từng phần và các mẫu trình soạn thảo.

Ở đâu đó trong bố cục của bạn, bạn cần gọi

@Html.RenderScripts()

Đây sẽ là nơi bất kỳ tệp tập lệnh và khối tập lệnh nào sẽ được xuất trong trang vì vậy tôi khuyên bạn nên đặt nó sau các tập lệnh chính của bạn trong bố cục và sau một phần tập lệnh (nếu bạn có).

Nếu bạn đang sử dụng Khung tối ưu hóa web với tính năng đóng gói, bạn có thể sử dụng quá tải

@Html.RenderScripts(Scripts.Render)

để phương pháp này được sử dụng để viết ra các tệp kịch bản.

Bây giờ, bất cứ lúc nào bạn muốn thêm tệp script hoặc khối trong một dạng xem, dạng xem một phần hoặc mẫu, chỉ cần sử dụng

@using (Html.BeginScriptContext())
{
  Html.AddScriptFile("~/Scripts/jquery.validate.js");
  Html.AddScriptBlock(
    @<script type="text/javascript">
       $(function() { $('#someField').datepicker(); });
     </script>
  );
}

Người trợ giúp đảm bảo rằng chỉ một tham chiếu tệp tập lệnh được hiển thị nếu được thêm nhiều lần và nó cũng đảm bảo rằng các tệp tập lệnh được hiển thị theo thứ tự dự kiến, tức là

  1. Bố trí
  2. Phân vùng và Mẫu (theo thứ tự xuất hiện trong chế độ xem, từ trên xuống dưới)

5

Bài đăng này thực sự giúp ích cho tôi vì vậy tôi nghĩ tôi sẽ đăng việc triển khai ý tưởng cơ bản của mình. Tôi đã giới thiệu một hàm trợ giúp có thể trả về các thẻ script để sử dụng trong hàm @ Html.Resource.

Tôi cũng đã thêm một lớp tĩnh đơn giản để tôi có thể sử dụng các biến đã nhập để xác định tài nguyên JS hoặc CSS.

public static class ResourceType
{
    public const string Css = "css";
    public const string Js = "js";
}

public static class HtmlExtensions
{
    public static IHtmlString Resource(this HtmlHelper htmlHelper, Func<object, dynamic> template, string Type)
    {
        if (htmlHelper.ViewContext.HttpContext.Items[Type] != null) ((List<Func<object, dynamic>>)htmlHelper.ViewContext.HttpContext.Items[Type]).Add(template);
        else htmlHelper.ViewContext.HttpContext.Items[Type] = new List<Func<object, dynamic>>() { template };

        return new HtmlString(String.Empty);
    }

    public static IHtmlString RenderResources(this HtmlHelper htmlHelper, string Type)
    {
        if (htmlHelper.ViewContext.HttpContext.Items[Type] != null)
        {
            List<Func<object, dynamic>> resources = (List<Func<object, dynamic>>)htmlHelper.ViewContext.HttpContext.Items[Type];

            foreach (var resource in resources)
            {
                if (resource != null) htmlHelper.ViewContext.Writer.Write(resource(null));
            }
        }

        return new HtmlString(String.Empty);
    }

    public static Func<object, dynamic> ScriptTag(this HtmlHelper htmlHelper, string url)
    {
        var urlHelper = new UrlHelper(htmlHelper.ViewContext.RequestContext);
        var script = new TagBuilder("script");
        script.Attributes["type"] = "text/javascript";
        script.Attributes["src"] = urlHelper.Content("~/" + url);
        return x => new HtmlString(script.ToString(TagRenderMode.Normal));
    }
}

Và đang được sử dụng

@Html.Resource(Html.ScriptTag("Areas/Admin/js/plugins/wysiwyg/jquery.wysiwyg.js"), ResourceType.Js)

Cảm ơn @Darin Dimitrov, người đã cung cấp câu trả lời cho câu hỏi của tôi ở đây .


2

Câu trả lời được đưa ra trong Điền phần dao cạo từ một phần bằng cách sử dụng RequireScriptHtmlHelper theo cùng một mô hình. Nó cũng có lợi ích là nó kiểm tra và ngăn chặn các tham chiếu trùng lặp đến cùng một URL Javascript và nó có một prioritytham số rõ ràng có thể được sử dụng để kiểm soát thứ tự.

Tôi đã mở rộng giải pháp này bằng cách thêm các phương pháp cho:

// use this for scripts to be placed just before the </body> tag
public static string RequireFooterScript(this HtmlHelper html, string path, int priority = 1) { ... }
public static HtmlString EmitRequiredFooterScripts(this HtmlHelper html) { ... }

// use this for CSS links
public static string RequireCSS(this HtmlHelper html, string path, int priority = 1) { ... }
public static HtmlString EmitRequiredCSS(this HtmlHelper html) { ... }

Tôi thích các giải pháp của Darin & eth0 vì chúng sử dụng HelperResultmẫu, cho phép các khối tập lệnh và CSS, không chỉ liên kết đến các tệp Javascript và CSS.


1

@Darin Dimitrov và @ eth0 câu trả lời để sử dụng với việc sử dụng mở rộng gói:

@Html.Resources(a => new HelperResult(b => b.Write( System.Web.Optimization.Scripts.Render("~/Content/js/formBundle").ToString())), "jsTop")
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.