`
dragon0929
  • 浏览: 77858 次
  • 性别: Icon_minigender_1
  • 来自: 北京
文章分类
社区版块
存档分类
最新评论

Smart GWT Sample (Form)

阅读更多

Form Validate; Form Field display&hide; input field filter; input field mask;Select box联动;

定制Validator;扩展DataSource Field Type;multiple forms

 

1. Form Validate

 

       private Widget validateTest() {
             DataSource dataSource = new DataSource();
             DataSourceTextField firstNameField = new DataSourceTextField("firstName", "First Name", 50, true);   
             DataSourceTextField lastNameField = new DataSourceTextField("lastName", "Last Name", 50, true);   
             DataSourceTextField emailField = new DataSourceTextField("email", "Email", 100, true);   
       
             RegExpValidator emailValidator = new RegExpValidator();   
             emailValidator.setErrorMessage("Invalid email address");   
             emailValidator.setExpression("^([a-zA-Z0-9_.\\-+])+@(([a-zA-Z0-9\\-])+\\.)+[a-zA-Z0-9]{2,4}$");   
                
             emailField.setValidators(emailValidator);   
             
            MaskValidator maskValidator = new MaskValidator();   
            maskValidator.setMask("^\\s*(1?)\\s*\\(?\\s*(\\d{3})\\s*\\)?\\s*-?\\s*(\\d{3})\\s*-?\\s*(\\d{4})\\s*$");   
            maskValidator.setTransformTo("$1($2) $3 - $4");   
               
            DataSourceTextField phoneField = new DataSourceTextField("phone");   
            phoneField.setTitle("Phone");   
            phoneField.setValidators(maskValidator);

 

            DataSourceIntegerField dsIntegerField = new DataSourceIntegerField("intField");  
            dsIntegerField.setTitle("Integer");
            
            DataSourcePasswordField passwordField = new DataSourcePasswordField("password", "Password", 20, true);    
            
            dataSource.setFields(firstNameField, lastNameField, emailField, phoneField, dsIntegerField, passwordField);
            
            final DynamicForm form = new DynamicForm();   
            form.setWidth(300);   
            form.setDataSource(dataSource);   
            form.setUseAllDataSourceFields(true);   
            
            HeaderItem header = new HeaderItem();   
            header.setDefaultValue("Registration Form");   
            PasswordItem passwordItem = new PasswordItem();   
            passwordItem.setName("password");   
            PasswordItem passwordItem2 = new PasswordItem();   
            passwordItem2.setName("password2");   
            passwordItem2.setTitle("Password Again");   
            passwordItem2.setRequired(true);   
            passwordItem2.setLength(20);   
            MatchesFieldValidator matchesValidator = new MatchesFieldValidator();   
            matchesValidator.setOtherField("password");   
            matchesValidator.setErrorMessage("Passwords do not match");           
            passwordItem2.setValidators(matchesValidator);   
      
            CheckboxItem acceptItem = new CheckboxItem();   
            acceptItem.setName("acceptTerms");   
            acceptItem.setTitle("I accept the terms of use.");   
            acceptItem.setWidth(150);   
      
            ButtonItem validateItem = new ButtonItem();   
            validateItem.setTitle("Validate");   
            validateItem.addClickHandler(new com.smartgwt.client.widgets.form.fields.events.ClickHandler() {   
                public void onClick(com.smartgwt.client.widgets.form.fields.events.ClickEvent event) {   
                    form.validate(false);  // 不验证hidden item(如伸展状态的section中的item);默认为false.
                }   
            });  
            
            form.setFields(header, passwordItem, passwordItem2, acceptItem, validateItem);   
            form.setValue("firstName", "Bob");   
            form.setValue("email", "bob@.com");   
            form.setValue("password", "sekrit");   
            form.setValue("password2", "fatfinger");   

            return form;

        }

 1)form.setUseAllDataSourceFields(true); 这行必须得有,我一开始没有设置,dataSource.setFields(), 绑定的字段,全部不显示,但是如果不和form.setFields() 同时使用,则没有这个现象。

2)dataSource.setFields()和 form.setFields() 中的password item似乎重复,我试着去掉其中的一个,  form 的 layout出现混乱,dataSource.setFields()和 form.setFields() 绑定的字段自动分成两组。另外就是dataSource.setFields() 中的passwordField 必须放在最后面,否则其的后面的fields会次序混乱。

 

关于上面两个问题,我查看了setUseAllDataSourceFields 的javadoc,看了半天,还是不太明白,以后慢慢研究。

If true, the set of fields given by the "default binding" (see DataBoundComponent.fields) is used, with any  fields specified in component.fields acting as overrides that can suppress or modify the display of individual fields, without having to list the entire set of fields that should be shown.
       If component.fields contains fields that are not found in the DataSource, they will be shown after the most recently referred to DataSource field. If the new fields appear first, they will be shown first.

 

 

2. 动态刷新form component

 

  CheckboxItem checkboxItem = new CheckboxItem();  
            checkboxItem.setTitle("display combobox");
            checkboxItem.setName("disCombox");
            checkboxItem.setValue(false);
            checkboxItem.setRedrawOnChange(true);

              
            ComboBoxItem cbItem = new ComboBoxItem();  
            cbItem.setTitle("Select");  
            cbItem.setHint("<nobr>A simple combobox</nobr>");  
            cbItem.setValueMap("Cat", "Dog", "Giraffe", "Goat", "Marmoset", "Mouse");
            cbItem.setVisible(false);
            cbItem.setShowIfCondition(new FormItemIfFunction() {  
                public boolean execute(FormItem item, Object value, DynamicForm form) {  
                    return (Boolean)form.getValue("disCombox");
                }  
            }); 

  click 'checkboxItem' to display or hide  'cbItem'

 

1)checkboxItem.setValue(false); 必须设置初始值,否则return (Boolean)form.getValue("disCombox"); 这一行会报null exception;

2) checkboxItem.setRedrawOnChange(true); 这行是关键,目的是改变这个元素的值,会刷新form;

   (javadoc: Setting to true causes the FormItem to be immediately redrawn with the new properties
      when its value changes)

 

还有一种改变form field显示状态的方法,如下:

 

CheckboxItem acceptItem = new CheckboxItem();  
            acceptItem.setName("acceptTerms");  
            acceptItem.setTitle("I accept the terms of use.");  
            acceptItem.setWidth(150);  
     
            final ButtonItem validateItem = new ButtonItem();  
            validateItem.setTitle("Validate");  
            validateItem.addClickHandler(new com.smartgwt.client.widgets.form.fields.events.ClickHandler() {  
                public void onClick(com.smartgwt.client.widgets.form.fields.events.ClickEvent event) {  
                    form.validate(false);  
                }  
            }); 
           
            acceptItem.addChangeHandler(new ChangeHandler() {  
                public void onChange(ChangeEvent event) {  
                    validateItem.setDisabled(!((Boolean) event.getValue()));  
                }  
            });

 

3.为输入框添加过滤器

   private Widget filterTest() {
            DataSource dataSource = new DataSource();
            DataSourceTextField firstNameField = new DataSourceTextField("firstName", "First Name", 50, true);  
            firstNameField.setAttribute("hint", "<nobr>Mapped to uppercase</nobr>");
            firstNameField.setAttribute("characterCasing", CharacterCasing.UPPER);

            dataSource.setFields(firstNameField);
           
            DynamicForm form = new DynamicForm();
            form.setDataSource(dataSource);
            form.setUseAllDataSourceFields(true);
           
            TextItem nameField = new TextItem("name", "Name");  
            nameField.setWidth(200);  
             nameField.setHint("Mapped to uppercase");  
            nameField.setCharacterCasing(CharacterCasing.UPPER);  

     
            TextItem commisionField = new TextItem("commission", "Commission");  
            commisionField.setWidth(100);  
            commisionField.setHint("Numeric only<br>[0-9.]");          
            commisionField.setKeyPressFilter("[0-9.]");  
     
            form.setFields(nameField, commisionField);
           
            return form;
        } 

    Note: 注意DataSource Field 设置property value的方式。

 

4.为输入框添加mask

    private Widget mastTest() {  
            DynamicForm form = new DynamicForm();  
            form.setWidth(400);  
     
            TextItem firstName = new TextItem("firstName", "First name");  
            firstName.setMask(">?<??????????????");  
            firstName.setHint("<nobr>>?<??????????????<nobr>");  
     
            TextItem lastName = new TextItem("lastName", "Last name");  
            lastName.setMask(">?<??????????????");
            lastName.setHint("<nobr>>?<??????????????<nobr>");
            TextItem stateField = new TextItem("state", "State");  
            stateField.setMask(">LL");  
            stateField.setHint("<nobr>>LL</nobr>");  
     
            TextItem phoneNumberField = new TextItem("phoneNo", "Phone No.");  
            phoneNumberField.setMask("(###) ###-####");  
            phoneNumberField.setHint("<nobr>(###) ###-####</nobr>");  
     
            DateItem dateField = new DateItem("dateItem", "Date");  
            dateField.setUseTextField(true);  
            dateField.setUseMask(true);  
     
            DateTimeItem dateTimeField = new DateTimeItem("dateTimeItem", "Date Time");  
            dateTimeField.setUseTextField(true);  
            dateTimeField.setUseMask(true);  
     
            TimeItem timeField = new TimeItem("timeItem", "Time");  
            timeField.setUseMask(true);  
     
            form.setFields(firstName, lastName, stateField, phoneNumberField, dateField, dateTimeField, timeField);  
     
            return form;
        }  

 

     效果和期望的不太一样,firstName和lastName的hint信息只显示两个符“ >? ”,而不能完全显示“>?<??????????????”,我用firebug查看源文件,也是只有这两个字符,不知道原因,以后再研究。

 

Mask Character 如下:

 

 

Character Description
0 Digit (0 through 9) or plus [+] or minus [-] signs
9 Digit or space
# Digit
L Letter (A through Z)
? Letter (A through Z) or space
A Letter or digit
a Letter or digit
C Any character or space
< Causes all characters that follow to be converted to lowercase
> Causes all characters that follow to be converted to uppercase

Any character not matching one of the above mask characters or that is escaped with a backslash (\) is considered to be a literal.

Custom mask characters can be defined by standard regular expression character set or range. For example, a hexadecimal color code mask could be:

  • Color: \#>[0-9A-F][0-9A-F][0-9A-F][0-9A-F][0-9A-F][0-9A-F]

 

5. Select Box 联动

 

private Widget selectRelationSelectTest() {  
             
            final DynamicForm form = new DynamicForm();  
            form.setWidth(500);  
            form.setNumCols(4);  
     
            final Map<String, String[]> departments = new HashMap<String, String[]>();  
            departments.put("Marketing", new String[]{"Advertising", "Community Relations"});  
            departments.put("Sales", new String[]{"Channel Sales", "Direct Sales"});  
            departments.put("Manufacturing", new String[]{"Design", "Development", "QA"});  
            departments.put("Services", new String[]{"Support", "Consulting"});  
     
            SelectItem divisionItem = new SelectItem();  
            divisionItem.setName("division");  
            divisionItem.setTitle("Division");  
            divisionItem.setValueMap("Marketing", "Sales", "Manufacturing", "Services");  
            divisionItem.addChangeHandler(new ChangeHandler() {  
                public void onChange(ChangeEvent event) {  
                    String selectedItem = (String) event.getValue();  
                    form.getField("department").setValueMap(departments.get(selectedItem));  
                }  
            });  
     
            SelectItem departmentItem = new SelectItem();  
            departmentItem.setName("department");  
            departmentItem.setTitle("Department");  
            departmentItem.setAddUnknownValues(false);  
     
            form.setItems(divisionItem, departmentItem);  
     
            return form;
        }  

  不知什么原因,我所有的SelectItem和combobox在chrome下都不能显示选择项,在firefox下正常,郁闷。

 

6. 定制validation

   private Widget customizeValidation() {  
             
            final DynamicForm form = new DynamicForm();  
            form.setWidth(250);  
            form.setTitleOrientation(TitleOrientation.TOP);  
     
            final RadioGroupItem radioGroupItem = new RadioGroupItem();  
            radioGroupItem.setName("willAttend");  
            radioGroupItem.setColSpan("*");  
            radioGroupItem.setRequired(true);  
            radioGroupItem.setVertical(false);  
            radioGroupItem.setValueMap("Yes", "No");  
            radioGroupItem.setRedrawOnChange(true);  
            radioGroupItem.setTitle("Will you be attending the meeting on April 4th? If no, please provide a reason");  
     
            TextItem textItem = new TextItem();  
            textItem.setName("reason");  
            textItem.setTitle("Reason");  
            RequiredIfValidator ifValidator = new RequiredIfValidator();  
            ifValidator.setExpression(new RequiredIfFunction() {  
                public boolean execute(FormItem formItem, Object value) {  
                    String valueStr = (String) radioGroupItem.getValue();  
                    return "No".equals(valueStr);  
                }  
            });  

            ifValidator.setErrorMessage("Please provide a reason");  
     
            textItem.setValidators(ifValidator);  
     
            ButtonItem buttonItem = new ButtonItem("validate", "Validate");  
            buttonItem.addClickHandler(new com.smartgwt.client.widgets.form.fields.events.ClickHandler() {  
                public void onClick(com.smartgwt.client.widgets.form.fields.events.ClickEvent event) {  
                    form.validate();  
                }  
            });  
     
            form.setFields(radioGroupItem, textItem, buttonItem);  
            return form; 
        }

 

上面的sample根据form中raio的value来定制RequiredIfValidator.

 

7.扩展DataSource的field type

 

This example demonstrates a DataSourceField that is based on a user created SimpleType . As illustrated in this sample, a user can create a reusable ZipCode SimpleType class with a regular expression based com.smartgwt.client.widgets.form.validator.Validator and then use this SimpleType in various DataSourceField definitions across their application for use with any DataBoundComponent like a ListGrid, TreeGrid, DynamicForm etc.

This is a powerful feature allows creation and reuse of domain specific "primitive" data types or types in the enterprises Common Data Model (CDM).

 

private Widget zipCodeTest() {  
             
            DataSource dataSource = new DataSource();  
     
            DataSourceField zipCodeField = new DataSourceField();  
            zipCodeField.setName("zipCode");  
            zipCodeField.setTitle("Zip Code");  
            zipCodeField.setType(new ZipCodeUSType());  
     
            dataSource.setFields(zipCodeField);  
              
            final DynamicForm boundForm = new DynamicForm();  
            boundForm.setWidth(300);  
            boundForm.setDataSource(dataSource);  
     
            IButton button = new IButton("Validate");  
            button.addClickHandler(new ClickHandler() {  
                public void onClick(ClickEvent event) {  
                    boundForm.validate();  
                }  
            });  
     
            VLayout layout = new VLayout(10);  
            layout.setMembers(boundForm, button);  
     
            return layout;
        }  
     
        public static class ZipCodeUSType extends SimpleType {  
            public ZipCodeUSType() {  
                super("zipCodeUS", FieldType.TEXT);  
     
                RegExpValidator validator = new RegExpValidator("^\\d{5}(-\\d{4})?$");  
                setValidators(validator);  
            }  
        } 

 

8. multiple forms

 

private Widget multipleForms() {  
            final ValuesManager vm = new ValuesManager();  
              
            final TabSet theTabs = new TabSet();  
            theTabs.setWidth(400);  
            theTabs.setHeight(250);  
              
            Tab item = new Tab();  
            item.setTitle("Item");  
              
            final DynamicForm form0 = new DynamicForm();  
            form0.setID("form0");  
            form0.setValuesManager(vm);  
              
            TextItem itemName = new TextItem();  
            itemName.setName("itemName");  
            itemName.setTitle("Item");  
              
            TextAreaItem description = new TextAreaItem();  
            description.setName("description");  
            description.setTitle("Description");  
              
            FloatItem price = new FloatItem();  
            price.setName("price");  
            price.setTitle("Price");  
            price.setDefaultValue("low");  
              
            form0.setFields(itemName, description, price);  
            item.setPane(form0);  
     
            Tab stock = new Tab();  
            stock.setTitle("Stock");  
              
            final DynamicForm form1 = new DynamicForm();  
            form1.setID("form1");  
            form1.setValuesManager(vm);  
              
            CheckboxItem inStock = new CheckboxItem();  
            inStock.setName("inStock");  
            inStock.setTitle("In Stock");  
              
            DateItem nextShipment = new DateItem();  
            nextShipment.setName("nextShipment");  
            nextShipment.setTitle("Next Shipment");  
            nextShipment.setUseTextField(true);  
            nextShipment.setDefaultValue(256);  
              
            form1.setFields(inStock, nextShipment);  
            stock.setPane(form1);  
              
            theTabs.setTabs(item, stock);  
              
            IButton submit = new IButton();  
            submit.setTitle("Submit");  
            submit.addClickHandler(new ClickHandler() {  
                public void onClick(ClickEvent event) {  
                    vm.setValues(new HashMap());  
                    vm.validate();  
                    if (form1.hasErrors()) {  
                        theTabs.selectTab(1);  
                    } else {  
                        theTabs.selectTab(0);  
                    }  
                }  
            });  
              
            VLayout vLayout = new VLayout();  
            vLayout.setMembersMargin(10);  
            vLayout.addMember(theTabs);  
            vLayout.addMember(submit);  
     
            return vLayout;
        } 

 

   此处的关键是ValuesManager ,下面是它的javadoc

ValuesManager

<!-- Generated by javadoc (build 1.5.0_19) on Sun Dec 13 17:34:16 EST 2009 -->

<script type="text/javascript"> function windowTitle() { parent.document.title=&quot;ValuesManager&quot;; } </script>

<noscript></noscript>

The ValuesManager manages data from multiple member forms.

If a single logical form needs to be separated into multiple DynamicForm instances for Layout purposes (for example, spanning one logical form across multiple Tabs), a ValuesManager can be used to make the forms act as one logical form, supporting all value-related APIs otherwise called on DynamicForm directly.

A ValuesManager has no visual representation - it is strictly a logical entity, and the member forms provide the user interface. You can initialize a ValuesManager with a set of member forms (by setting members at init) or add and remove member forms dynamically.

Calling setValues(java.util.Map) on a ValuesManager will automatically route new field values to whichever member form is showing an editor for that field. Likewise, calling validate() will validate all member forms, and saveData() will initiate a save operation which aggregates values from all member forms.

Like a DynamicForm, a ValuesManager can be databound by setting dataSource . In this case all member forms must also be bound to the same DataSource.

In general, when working with a ValuesManager and its member forms, call APIs on the ValuesManager whenever you are dealing with values that span multiple forms, and only call APIs on member forms that are specific to that form or its fields.

Note that, just as a DynamicForm can track values that are not shown in any FormItem, a ValuesManager may track values for which there is no FormItem in any member form. However, when using a ValuesManager these extra values are only allowed on the ValuesManager itself. Member forms will not track values for which they do not have FormItems.

分享到:
评论
1 楼 Durian 2011-03-15  
乱七八糟的

相关推荐

    smartgwt官方实例

    SmartGWT是一个强大的Java框架,用于构建富互联网应用程序(RIA)。这个官方实例是学习和理解SmartGWT功能和用法的宝贵资源,尤其适合自学者。"Showcase"通常指的是一个展示各种组件、特性和功能的集合,让我们深入...

    SmartGwt学习文档

    SmartGwt是一款强大的Java库,专门用于构建富互联网应用程序(RIA,Rich Internet Applications)。它基于Google的GWT(Google Web Toolkit)框架,并扩展了其功能,提供了丰富的UI组件和更高级的客户端性能优化。...

    SmartGWT2.0 API

    SmartGWT 2.0 API 是一款基于Google Web Toolkit (GWT) 2.0的高级组件库,它为开发者提供了丰富的用户界面组件和强大的功能,以构建高性能、交互式的Web应用程序。SmartGWT 2.0 在其前身的基础上进行了多方面的改进...

    smartGWT 3.1 最新版本

    在本篇文章中,我们将深入探讨SmartGWT 3.1的关键特性、API使用、源码解析以及Sample示例。 1. **SmartGWT框架概述** SmartGWT是一个基于Google Web Toolkit (GWT) 的扩展框架,它通过提供大量预定义的用户界面...

    smartgwt-1.3

    SmartGWT是一个强大的Java库,专门用于构建富互联网应用程序(RIA)。这个名为"smartgwt-1.3"的压缩包文件很可能包含了SmartGWT框架的1.3版本的源代码、库文件和其他相关资源。SmartGWT是基于Google Web Toolkit ...

    smartGWT开发环境搭建(完整工程)

    SmartGWT是一个强大的Java框架,用于构建富互联网应用程序(RIA)。它基于Google Web Toolkit (GWT) 并扩展了其功能,提供了丰富的组件库和高级的UI设计工具。本教程将详细阐述如何搭建SmartGWT的开发环境,以及如何...

    基于Maven的SmartGWT项目示例

    本示例项目“基于Maven的SmartGWT项目”就是这样的一个实践,它结合了两个强大的技术:Maven作为项目管理和构建工具,以及SmartGWT作为前端UI框架。下面我们将深入探讨这两个技术以及它们如何协同工作。 首先,...

    SmartGWT 12.0

    SmartGWT 12.0 是一款强大的Java框架,专为构建富互联网应用程序(Rich Internet Applications,RIAs)而设计。这个最新版本提供了一系列增强的功能和优化,旨在提高开发效率和用户体验。SmartGWT库的核心在于它将...

    smartGWT最新zip

    SmartGWT是一个强大的Java库,专门用于构建富互联网应用程序(RIA)。它基于Google Web Toolkit (GWT) 平台,并提供了丰富的用户界面组件和优化工具,使得开发人员能够轻松创建功能丰富的、交互性强的Web应用。GWT...

    smartgwt2.4 最新发布

    SmartGWT 2.4 是一个强大的开源框架,主要用于构建企业级的富互联网应用程序(Rich Internet Applications,简称RIA)。这个框架基于Google Web Toolkit (GWT) 并且扩展了其功能,提供了丰富的组件库和高级特性,...

    smartgwt5.0

    SmartGWT 是封装了 SmartClient 的 GWT API。而 SmartClient 是一个开源的企业级 Ajax 开发框架。 Google Web Toolkit 的发布,大大降低了 Java 开发人员进行 Web 开发的门槛。然而 GWT 本身提供的控件及功能相对...

    smartGwt学习笔记

    在SmartGWT的学习和开发过程中,经常会遇到各种挑战和问题,比如本文提到的“分页问题GridPager”。SmartGwt是一个强大的Java库,用于构建基于Web的用户界面,它提供了丰富的组件,包括ListGrid,一个功能强大的表格...

    smart GWT 3.1

    SmartGWT 是封装了 SmartClient 的 GWT API。SmartGWT 有如下特色: 丰富的控件。很多较为复杂的常用界面都被包装成简单易用的控件。比如可 编辑的树形表格、查询常用的过滤器创建器和类似 Google Calendar 的日历...

    smartgwt + spring + hibernate

    SmartGWT + Spring + Hibernate 是一个常见的企业级应用开发组合,它们各自在Web应用程序开发中扮演着重要角色。SmartGWT是基于GWT(Google Web Toolkit)的一个强大的UI库,提供丰富的用户界面组件和高性能的...

    SmartGWT 入门 SmartGWT 入门

    ### SmartGWT 入门详解 #### 一、SmartGWT 概述 **SmartGWT** 是一款基于 **Google Web Toolkit (GWT)** 的高级Web应用开发框架,旨在简化企业级Web 2.0应用的开发过程。自从 **GWT** 发布以来,它吸引了大量的...

    smartGWT

    "SmartGWT"是一个强大的Java库,用于构建企业级的Web应用程序。它基于Google Web Toolkit (GWT) 并提供了一系列高级组件和功能,旨在简化开发过程,提高开发效率。SmartGWT尤其适合那些需要创建数据密集型、交互性强...

    SmartGWT 快速开发文档(Quick Start Guide)

    ### SmartGWT 快速开发文档 (Quick Start Guide) 关键知识点详解 #### 一、文档概述 **SmartGWT** 是一款强大的基于 Java 的 Web 应用前端框架,它支持构建高性能的企业级应用程序。本快速开发文档为 SmartGwtEE ...

    smartgwt最新版本GWT的DEMO

    1. **组件使用**:SmartGWT中的组件如Grid(表格)、Form(表单)、Tree(树形结构)、Dialog(对话框)等,都有详细的示例展示。开发者可以通过这些例子了解如何创建、配置和操作这些组件。 2. **数据绑定**:...

    SmartGwt 之原生 Desktop(与gxt无关)

    《SmartGwt 原生 Desktop 深度解析》 SmartGwt 是一款基于 Java 的开源库,它为开发人员提供了丰富的组件和工具,用于构建高性能、富交互式的 Web 应用程序。在 SmartGwt 中,原生的 Desktop 功能是一个强大的特性...

    smartgwt-2.0API.CHM

    smartgwt-2.0API.CHM 2009年12月18日 ... SmartGWT 2.0发布下载了

Global site tag (gtag.js) - Google Analytics