Đệ quy trong chỉ thị góc


178

Có một vài câu hỏi và chỉ thị góc đệ quy phổ biến ngoài kia, tất cả đều đi đến một trong những giải pháp sau:

Cái đầu tiên có vấn đề là bạn không thể loại bỏ mã được biên dịch trước đó trừ khi bạn quản lý toàn diện quy trình biên dịch thủ công. Cách tiếp cận thứ hai có vấn đề là ... không phải là một chỉ thị và bỏ lỡ các khả năng mạnh mẽ của nó, nhưng khẩn cấp hơn, nó không thể được tham số hóa giống như một chỉ thị có thể được; nó chỉ đơn giản là bị ràng buộc với một cá thể bộ điều khiển mới.

Tôi đã chơi bằng tay khi thực hiện một angular.bootstraphoặc @compile()trong chức năng liên kết, nhưng điều đó khiến tôi gặp phải vấn đề theo dõi thủ công các yếu tố để xóa và thêm.

Có cách nào tốt để có một mẫu đệ quy được tham số hóa để quản lý việc thêm / xóa các phần tử để phản ánh trạng thái thời gian chạy không? Điều đó có nghĩa là, một cây có nút nút thêm / xóa và một số trường đầu vào có giá trị được truyền xuống các nút con của nút. Có lẽ một sự kết hợp của cách tiếp cận thứ hai với phạm vi chuỗi (nhưng tôi không biết làm thế nào để làm điều này)?

Câu trả lời:


316

Lấy cảm hứng từ các giải pháp được mô tả trong chuỗi được đề cập bởi @ dnc253, tôi đã trừu tượng hóa chức năng đệ quy thành một dịch vụ .

module.factory('RecursionHelper', ['$compile', function($compile){
    return {
        /**
         * Manually compiles the element, fixing the recursion loop.
         * @param element
         * @param [link] A post-link function, or an object with function(s) registered via pre and post properties.
         * @returns An object containing the linking functions.
         */
        compile: function(element, link){
            // Normalize the link parameter
            if(angular.isFunction(link)){
                link = { post: link };
            }

            // Break the recursion loop by removing the contents
            var contents = element.contents().remove();
            var compiledContents;
            return {
                pre: (link && link.pre) ? link.pre : null,
                /**
                 * Compiles and re-adds the contents
                 */
                post: function(scope, element){
                    // Compile the contents
                    if(!compiledContents){
                        compiledContents = $compile(contents);
                    }
                    // Re-add the compiled contents to the element
                    compiledContents(scope, function(clone){
                        element.append(clone);
                    });

                    // Call the post-linking function, if any
                    if(link && link.post){
                        link.post.apply(null, arguments);
                    }
                }
            };
        }
    };
}]);

Được sử dụng như sau:

module.directive("tree", ["RecursionHelper", function(RecursionHelper) {
    return {
        restrict: "E",
        scope: {family: '='},
        template: 
            '<p>{{ family.name }}</p>'+
            '<ul>' + 
                '<li ng-repeat="child in family.children">' + 
                    '<tree family="child"></tree>' +
                '</li>' +
            '</ul>',
        compile: function(element) {
            // Use the compile function from the RecursionHelper,
            // And return the linking function(s) which it returns
            return RecursionHelper.compile(element);
        }
    };
}]);

Xem Plunker này cho một bản demo. Tôi thích giải pháp này nhất vì:

  1. Bạn không cần một lệnh đặc biệt làm cho html của bạn không sạch sẽ.
  2. Logic đệ quy được trừu tượng hóa trong dịch vụ RecursionHelper, vì vậy bạn giữ cho các lệnh của mình sạch sẽ.

Cập nhật: Kể từ Angular 1.5.x, không yêu cầu thêm thủ thuật nào, nhưng chỉ hoạt động với mẫu , không phải với templateUrl


3
Cảm ơn, giải pháp tuyệt vời! thực sự sạch sẽ và làm việc ra khỏi hộp để tôi thực hiện đệ quy giữa hai chỉ thị bao gồm công việc của nhau.
jssebastian

6
Vấn đề ban đầu là khi bạn sử dụng các lệnh đệ quy AngularJS sẽ đi vào một vòng lặp vô tận. Mã này phá vỡ vòng lặp này bằng cách xóa nội dung trong sự kiện biên dịch lệnh, đồng thời biên dịch và thêm lại nội dung trong sự kiện liên kết của lệnh.
Mark Lagendijk

15
Trong ví dụ của bạn, bạn có thể thay thế compile: function(element) { return RecursionHelper.compile(element); }bằng compile: RecursionHelper.compile.
Paolo Moretti

1
Điều gì nếu bạn muốn mẫu được đặt trong một tập tin bên ngoài?
CodyBugstein 4/03/2015

2
Điều này là thanh lịch theo nghĩa là nếu / khi lõi Angular thực hiện một hỗ trợ tương tự, bạn có thể chỉ cần loại bỏ trình bao bọc biên dịch tùy chỉnh và tất cả các mã còn lại sẽ giữ nguyên.
Carlo Bonamico

25

Tự thêm các yếu tố và biên dịch chúng chắc chắn là một cách tiếp cận hoàn hảo. Nếu bạn sử dụng ng-repeat thì bạn sẽ không phải xóa các phần tử theo cách thủ công.

Bản trình diễn: http://jsfiddle.net/KNM4q/113/

.directive('tree', function ($compile) {
return {
    restrict: 'E',
    terminal: true,
    scope: { val: '=', parentData:'=' },
    link: function (scope, element, attrs) {
        var template = '<span>{{val.text}}</span>';
        template += '<button ng-click="deleteMe()" ng-show="val.text">delete</button>';

        if (angular.isArray(scope.val.items)) {
            template += '<ul class="indent"><li ng-repeat="item in val.items"><tree val="item" parent-data="val.items"></tree></li></ul>';
        }
        scope.deleteMe = function(index) {
            if(scope.parentData) {
                var itemIndex = scope.parentData.indexOf(scope.val);
                scope.parentData.splice(itemIndex,1);
            }
            scope.val = {};
        };
        var newElement = angular.element(template);
        $compile(newElement)(scope);
        element.replaceWith(newElement);
    }
}
});

1
Tôi đã cập nhật tập lệnh của bạn để nó chỉ có một lệnh. jsfiddle.net/KNM4q/103 Làm thế nào chúng ta có thể làm cho nút xóa đó hoạt động?
Benny Bottema

Rất đẹp! Tôi đã rất thân thiết, nhưng không có @poseition (Tôi nghĩ rằng tôi có thể tìm thấy nó với ParentData [val]. Nếu bạn cập nhật câu trả lời của mình với phiên bản cuối cùng ( jsfiddle.net/KNM4q/111 ) tôi sẽ chấp nhận nó.
Benny Bottema

12

Tôi không biết chắc chắn liệu giải pháp này có được tìm thấy trong một trong những ví dụ bạn liên kết hoặc cùng một khái niệm cơ bản hay không, nhưng tôi có nhu cầu về một chỉ thị đệ quy và tôi đã tìm thấy một giải pháp tuyệt vời, dễ dàng .

module.directive("recursive", function($compile) {
    return {
        restrict: "EACM",
        priority: 100000,
        compile: function(tElement, tAttr) {
            var contents = tElement.contents().remove();
            var compiledContents;
            return function(scope, iElement, iAttr) {
                if(!compiledContents) {
                    compiledContents = $compile(contents);
                }
                iElement.append(
                    compiledContents(scope, 
                                     function(clone) {
                                         return clone; }));
            };
        }
    };
});

module.directive("tree", function() {
    return {
        scope: {tree: '='},
        template: '<p>{{ tree.text }}</p><ul><li ng-repeat="child in tree.children"><recursive><span tree="child"></span></recursive></li></ul>',
        compile: function() {
            return  function() {
            }
        }
    };
});​

Bạn nên tạo lệnh recursivevà sau đó bọc nó xung quanh phần tử thực hiện cuộc gọi đệ quy.


1
@MarkError và @ dnc253 điều này rất hữu ích, tuy nhiên tôi luôn nhận được lỗi sau:[$compile:multidir] Multiple directives [tree, tree] asking for new/isolated scope on: <recursive tree="tree">
Jack

1
Nếu bất kỳ ai khác gặp phải lỗi này, chỉ cần bạn (hoặc Yoeman) không bao gồm bất kỳ tệp JavaScript nào nhiều lần. Bằng cách nào đó, tệp main.js của tôi được đưa vào hai lần và do đó hai lệnh có cùng tên được tạo. Sau khi loại bỏ một trong các JS bao gồm, mã đã hoạt động.
Jack

2
@Jack Cảm ơn bạn đã chỉ ra rằng. Chỉ cần dành một số giờ rắc rối để xử lý vấn đề này và nhận xét của bạn đã chỉ cho tôi đi đúng hướng. Đối với người dùng ASP.NET sử dụng dịch vụ gói, hãy đảm bảo rằng bạn không có phiên bản rút gọn cũ của tệp trong thư mục trong khi bạn sử dụng ký tự đại diện bao gồm trong gói.
Beyers

Đối với tôi, phần tử là cần thiết để thêm vào bên trong gọi lại như:. compiledContents(scope,function(clone) { iElement.append(clone); });Ngược lại, "yêu cầu" bộ điều khiển ed không được xử lý chính xác và lỗi: Error: [$compile:ctreq] Controller 'tree', required by directive 'subTreeDirective', can't be found!nguyên nhân.
Tsuneo Yoshioka

Tôi đang cố gắng tạo cấu trúc cây với js góc nhưng bị mắc kẹt với điều đó.
Học tập-Overthinker-Conf bối rối

10

Kể từ Angular 1.5.x, không cần thêm thủ thuật nào, những điều sau đây đã được thực hiện. Không cần nhiều hơn cho công việc bẩn xung quanh!

Phát hiện này là một sản phẩm của việc tôi tìm kiếm một giải pháp tốt hơn / sạch hơn cho một chỉ thị đệ quy. Bạn có thể tìm thấy nó ở đây https://jsfiddle.net/cattails27/5j5au76c/ . Nó hỗ trợ cho đến nay là 1.3.x.

angular.element(document).ready(function() {
  angular.module('mainApp', [])
    .controller('mainCtrl', mainCtrl)
    .directive('recurv', recurveDirective);

  angular.bootstrap(document, ['mainApp']);

  function recurveDirective() {
    return {
      template: '<ul><li ng-repeat="t in tree">{{t.sub}}<recurv tree="t.children"></recurv></li></ul>',
      scope: {
        tree: '='
      },
    }
  }

});

  function mainCtrl() {
    this.tree = [{
      title: '1',
      sub: 'coffee',
      children: [{
        title: '2.1',
        sub: 'mocha'
      }, {
        title: '2.2',
        sub: 'latte',
        children: [{
          title: '2.2.1',
          sub: 'iced latte'
        }]
      }, {
        title: '2.3',
        sub: 'expresso'
      }, ]
    }, {
      title: '2',
      sub: 'milk'
    }, {
      title: '3',
      sub: 'tea',
      children: [{
        title: '3.1',
        sub: 'green tea',
        children: [{
          title: '3.1.1',
          sub: 'green coffee',
          children: [{
            title: '3.1.1.1',
            sub: 'green milk',
            children: [{
              title: '3.1.1.1.1',
              sub: 'black tea'
            }]
          }]
        }]
      }]
    }];
  }
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.5.8/angular.min.js"></script>
<div>
  <div ng-controller="mainCtrl as vm">
    <recurv tree="vm.tree"></recurv>
  </div>
</div>


1
Cảm ơn vì điều đó. Bạn có thể liên kết tôi với các thay đổi đã giới thiệu tính năng này? Cảm ơn!
Steven

Sử dụng góc 1.5.x là rất quan trọng. 1.4.x sẽ không hoạt động và thực sự là phiên bản được cung cấp trong jsfiddle.
Paqman

trong jsfiddle jsfiddle.net/cattails27/5j5au76c không có cùng mã của câu trả lời này ... có đúng không? tôi đang thiếu gì
Paolo Biavati


4

Sau khi sử dụng một số cách giải quyết trong một thời gian, tôi đã liên tục quay lại vấn đề này.

Tôi không hài lòng với giải pháp dịch vụ vì nó hoạt động cho các lệnh có thể tiêm dịch vụ nhưng không hoạt động đối với các đoạn mẫu ẩn danh.

Tương tự, các giải pháp phụ thuộc vào cấu trúc khuôn mẫu cụ thể bằng cách thực hiện thao tác DOM trong chỉ thị là quá cụ thể và dễ vỡ.

Tôi có những gì tôi tin là một giải pháp chung đóng gói đệ quy như một chỉ thị của chính nó can thiệp tối thiểu với bất kỳ chỉ thị nào khác và có thể được sử dụng ẩn danh.

Dưới đây là một minh chứng mà bạn cũng có thể chơi xung quanh tại plnkr: http://plnkr.co/edit/MSiwnDFD81HAOXWvQWIM

var hCollapseDirective = function () {
  return {
    link: function (scope, elem, attrs, ctrl) {
      scope.collapsed = false;
      scope.$watch('collapse', function (collapsed) {
        elem.toggleClass('collapse', !!collapsed);
      });
    },
    scope: {},
    templateUrl: 'collapse.html',
    transclude: true
  }
}

var hRecursiveDirective = function ($compile) {
  return {
    link: function (scope, elem, attrs, ctrl) {
      ctrl.transclude(scope, function (content) {
        elem.after(content);
      });
    },
    controller: function ($element, $transclude) {
      var parent = $element.parent().controller('hRecursive');
      this.transclude = angular.isObject(parent)
        ? parent.transclude
        : $transclude;
    },
    priority: 500,  // ngInclude < hRecursive < ngIf < ngRepeat < ngSwitch
    require: 'hRecursive',
    terminal: true,
    transclude: 'element',
    $$tlb: true  // Hack: allow multiple transclusion (ngRepeat and ngIf)
  }
}

angular.module('h', [])
.directive('hCollapse', hCollapseDirective)
.directive('hRecursive', hRecursiveDirective)
/* Demo CSS */
* { box-sizing: border-box }

html { line-height: 1.4em }

.task h4, .task h5 { margin: 0 }

.task { background-color: white }

.task.collapse {
  max-height: 1.4em;
  overflow: hidden;
}

.task.collapse h4::after {
  content: '...';
}

.task-list {
  padding: 0;
  list-style: none;
}


/* Collapse directive */
.h-collapse-expander {
  background: inherit;
  position: absolute;
  left: .5px;
  padding: 0 .2em;
}

.h-collapse-expander::before {
  content: '•';
}

.h-collapse-item {
  border-left: 1px dotted black;
  padding-left: .5em;
}

.h-collapse-wrapper {
  background: inherit;
  padding-left: .5em;
  position: relative;
}
<!DOCTYPE html>
<html>

  <head>
    <link href="collapse.css" rel="stylesheet" />
    <link href="style.css" rel="stylesheet" />
    <script data-require="angular.js@1.3.15" data-semver="1.3.15" src="https://code.angularjs.org/1.3.15/angular.js"></script>
    <script src="//cdnjs.cloudflare.com/ajax/libs/jquery/2.1.1/jquery.min.js" data-semver="2.1.1" data-require="jquery@*"></script>
    <script src="script.js"></script>
    <script>
      function AppController($scope) {
        $scope.toggleCollapsed = function ($event) {
          $event.preventDefault();
          $event.stopPropagation();
          this.collapsed = !this.collapsed;
        }
        
        $scope.task = {
          name: 'All tasks',
          assignees: ['Citizens'],
          children: [
            {
              name: 'Gardening',
              assignees: ['Gardeners', 'Horticulture Students'],
              children: [
                {
                  name: 'Pull weeds',
                  assignees: ['Weeding Sub-committee']
                }
              ],
            },
            {
              name: 'Cleaning',
              assignees: ['Cleaners', 'Guests']
            }
          ]
        }
      }
      
      angular.module('app', ['h'])
      .controller('AppController', AppController)
    </script>
  </head>

  <body ng-app="app" ng-controller="AppController">
    <h1>Task Application</h1>
    
    <p>This is an AngularJS application that demonstrates a generalized
    recursive templating directive. Use it to quickly produce recursive
    structures in templates.</p>
    
    <p>The recursive directive was developed in order to avoid the need for
    recursive structures to be given their own templates and be explicitly
    self-referential, as would be required with ngInclude. Owing to its high
    priority, it should also be possible to use it for recursive directives
    (directives that have templates which include the directive) that would
    otherwise send the compiler into infinite recursion.</p>
    
    <p>The directive can be used alongside ng-if
    and ng-repeat to create recursive structures without the need for
    additional container elements.</p>
    
    <p>Since the directive does not request a scope (either isolated or not)
    it should not impair reasoning about scope visibility, which continues to
    behave as the template suggests.</p>
    
    <p>Try playing around with the demonstration, below, where the input at
    the top provides a way to modify a scope attribute. Observe how the value
    is visible at all levels.</p>
    
    <p>The collapse directive is included to further demonstrate that the
    recursion can co-exist with other transclusions (not just ngIf, et al)
    and that sibling directives are included on the recursive due to the
    recursion using whole 'element' transclusion.</p>
    
    <label for="volunteer">Citizen name:</label>
    <input id="volunteer" ng-model="you" placeholder="your name">
    <h2>Tasks</h2>
    <ul class="task-list">
      <li class="task" h-collapse h-recursive>
        <h4>{{task.name}}</h4>
        <h5>Volunteers</h5>
        <ul>
          <li ng-repeat="who in task.assignees">{{who}}</li>
          <li>{{you}} (you)</li>
        </ul>
        <ul class="task-list">
          <li h-recursive ng-repeat="task in task.children"></li>
        </ul>
      <li>
    </ul>
    
    <script type="text/ng-template" id="collapse.html">
      <div class="h-collapse-wrapper">
        <a class="h-collapse-expander" href="#" ng-click="collapse = !collapse"></a>
        <div class="h-collapse-item" ng-transclude></div>
      </div>
    </script>
  </body>

</html>


2

Bây giờ Angular 2.0 đã được đưa ra trong bản xem trước, tôi nghĩ sẽ ổn khi thêm một thay thế Angular 2.0 vào hỗn hợp. Ít nhất nó sẽ có lợi cho mọi người sau này:

Khái niệm chính là xây dựng một mẫu đệ quy với một tham chiếu tự:

<ul>
    <li *for="#dir of directories">

        <span><input type="checkbox" [checked]="dir.checked" (click)="dir.check()"    /></span> 
        <span (click)="dir.toggle()">{{ dir.name }}</span>

        <div *if="dir.expanded">
            <ul *for="#file of dir.files">
                {{file}}
            </ul>
            <tree-view [directories]="dir.directories"></tree-view>
        </div>
    </li>
</ul>

Sau đó, bạn liên kết một đối tượng cây vào mẫu và xem đệ quy chăm sóc phần còn lại. Dưới đây là một ví dụ đầy đủ: http://www.syntaxsuccess.com/viewarticle/recursive-treeview-in-angular-2.0


2

Có một cách giải quyết thực sự đơn giản cho việc này mà không yêu cầu chỉ thị nào cả.

Theo nghĩa đó, có thể nó thậm chí không phải là giải pháp cho vấn đề ban đầu nếu bạn cho rằng bạn cần chỉ thị, nhưng đó là giải pháp nếu bạn muốn cấu trúc GUI đệ quy với cấu trúc phụ được tham số hóa của GUI. Đó có lẽ là những gì bạn muốn.

Giải pháp dựa trên việc chỉ sử dụng ng-controller, ng-init và ng-gộp. Chỉ cần làm như sau, giả sử rằng bộ điều khiển của bạn được gọi là "MyContoder", mẫu của bạn được đặt trong myTemplate.html và bạn có một hàm khởi tạo trên bộ điều khiển của mình được gọi là init, lấy đối số A, B và C, làm cho nó có thể parametrize bộ điều khiển của bạn. Sau đó, giải pháp như sau:

myTemplate.htmllm:

<div> 
    <div>Hello</div>
    <div ng-if="some-condition" ng-controller="Controller" ng-init="init(A, B, C)">
       <div ng-include="'myTemplate.html'"></div>
    </div>
</div>

Tôi đã tìm thấy bởi sự trùng hợp đơn giản rằng loại cấu trúc này có thể được thực hiện đệ quy như bạn muốn trong góc vani đơn giản. Chỉ cần làm theo mẫu thiết kế này và bạn có thể sử dụng các cấu trúc UI đệ quy mà không cần bất kỳ sự biên dịch nâng cao nào, v.v.

Trong bộ điều khiển của bạn:

$scope.init = function(A, B, C) {
   // Do something with A, B, C
   $scope.D = A + B; // D can be passed on to other controllers in myTemplate.html
} 

Nhược điểm duy nhất tôi có thể thấy là cú pháp khó hiểu mà bạn phải đưa ra.


Tôi e rằng điều này không giải quyết được vấn đề theo một cách khá cơ bản: Với cách tiếp cận này, bạn sẽ cần biết độ sâu của đệ quy lên phía trước để có đủ bộ điều khiển trong myTemplate.html
Stewart_R

Thật ra, bạn không. Vì tệp của bạn myTemplate.html chứa một tham chiếu tự đến myTemplate.html bằng cách sử dụng ng-include (nội dung html ở trên là nội dung của myTemplate.html, có lẽ không được nêu rõ). Bằng cách đó, nó trở nên thực sự đệ quy. Tôi đã sử dụng các kỹ thuật trong sản xuất.
erobwen

Ngoài ra, có lẽ không được nêu rõ là bạn cũng cần sử dụng ng-if ở đâu đó để chấm dứt đệ quy. Vì vậy, myTemplate.html của bạn sau đó có dạng như được cập nhật trong nhận xét của tôi.
erobwen

0

Bạn có thể sử dụng công cụ tiêm góc-góc cho đó: https://github.com/knyga/angular-recursion-injection

Cho phép bạn làm tổ sâu không giới hạn với điều hòa. Chỉ biên dịch lại nếu cần và chỉ biên dịch đúng các phần tử. Không có phép thuật trong mã.

<div class="node">
  <span>{{name}}</span>

  <node--recursion recursion-if="subNode" ng-model="subNode"></node--recursion>
</div>

Một trong những điều cho phép nó hoạt động nhanh hơn và đơn giản hơn các giải pháp khác là hậu tố "--recursion".


0

Tôi đã kết thúc việc tạo ra một bộ các chỉ thị cơ bản cho đệ quy.

IMO Nó cơ bản hơn nhiều so với giải pháp được tìm thấy ở đây, và cũng linh hoạt nếu không hơn, vì vậy chúng tôi không bị ràng buộc sử dụng các cấu trúc UL / LI, v.v ... Nhưng rõ ràng những điều đó có ý nghĩa để sử dụng, tuy nhiên các chỉ thị không biết về điều này thực tế...

Một ví dụ siêu đơn giản sẽ là:

<ul dx-start-with="rootNode">
  <li ng-repeat="node in $dxPrior.nodes">
    {{ node.name }}
    <ul dx-connect="node"/>
  </li>
</ul>

Việc triển khai 'dx-start-with' an 'dx-connect' được tìm thấy tại: https://github.com/dotJEM/angular-tree

Điều này có nghĩa là bạn không phải tạo 8 chỉ thị nếu bạn cần 8 bố cục khác nhau.

Để tạo chế độ xem dạng cây trên đầu trang, nơi bạn có thể thêm hoặc xóa các nút sau đó sẽ khá đơn giản. Như trong: http://codepen.io/anon/pen/BjXGbY?editors=1010

angular
  .module('demo', ['dotjem.angular.tree'])
  .controller('AppController', function($window) {

this.rootNode = {
  name: 'root node',
  children: [{
    name: 'child'
  }]
};

this.addNode = function(parent) {
  var name = $window.prompt("Node name: ", "node name here");
  parent.children = parent.children || [];
  parent.children.push({
    name: name
  });
}

this.removeNode = function(parent, child) {
  var index = parent.children.indexOf(child);
  if (index > -1) {
    parent.children.splice(index, 1);
  }
}

  });
<div ng-app="demo" ng-controller="AppController as app">
  HELLO TREE
  <ul dx-start-with="app.rootNode">
<li><button ng-click="app.addNode($dxPrior)">Add</button></li>
<li ng-repeat="node in $dxPrior.children">
  {{ node.name }} 
  <button ng-click="app.removeNode($dxPrior, node)">Remove</button>
  <ul dx-connect="node" />
</li>
  </ul>

  <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.0/angular.min.js"></script>
  <script src="https://rawgit.com/dotJEM/angular-tree-bower/master/dotjem-angular-tree.min.js"></script>

</div>

Từ thời điểm này, bộ điều khiển và mẫu có thể được gói trong chỉ thị riêng của nó nếu ai đó muốn.

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.