Util 應用框架 UI 全新升級

Util UI 已經開發多年, 並在多家公司的項目使用.

不過一直以來, Util UI 存在一些缺陷, 始終未能解決.

最近幾個月, Util 團隊下定決心, 終於徹底解決了所有已知缺陷.

Util 應用框架 UI 介紹

Util 應用框架 UI 建立在 Angular , Ng-Zorro, Ng-Alain 基礎之上, 用於開發企業中後臺.

Util 應用框架 UI 的特點

  • 簡潔

    Util UI 通常可以將複雜組件的 html 代碼量壓縮 3 - 10 倍,從而使項目的可維護性大幅提升.

    下面以查詢表單爲例進行對比.

    先看效果演示.

    Util UI 的標籤使用 TagHelper 進行封裝 ,代碼如下.

    <util-card borderless="true" class="searchForm">
        <util-search-form label-width="120">
            <util-row gutter="24">
                <util-column>
                    <util-input id="code" name="code"  ng-model="queryParam.code" label-text="identity.application.code"/>
                </util-column>
                <util-column>
                    <util-input id="name" name="name"  ng-model="queryParam.name" label-text="identity.application.name"/>
                </util-column>
                <util-column>
                    <util-select id="enabled" name="enabled"  ng-model="queryParam.enabled" label-text="identity.application.enabled"/>
                </util-column>
                <util-column>
                    <util-input id="remark" name="remark"  ng-model="queryParam.remark" label-text="identity.application.remark"/>
                </util-column>
                <util-column>
                <util-column>
                <util-range-picker id="begin_creation_time" name="begin_creation_time"  
                    label-text="util.beginCreationTime"
                    begin-date="queryParam.beginCreationTime" end-date="queryParam.endCreationTime"/>
                </util-column>
                <util-column>
                    <util-range-picker id="begin_last_modification_time" name="begin_last_modification_time"
                        label-text="util.beginLastModificationTime"
                        begin-date="queryParam.beginLastModificationTime" end-date="queryParam.endLastModificationTime" />
                </util-column>
                <util-column class="mb-md">
                    <util-flex justify="FlexEnd" align="Center" gap="Small">
                        <util-button id="btnRefresh" icon="Sync" on-click="refresh(btnRefresh)" text-reset="true"></util-button>
                        <util-button id="btnQuery" type="Primary" icon="Search" on-click="query(btnQuery)" text-query="true"></util-button>                        
                        <util-a is-search="true" class="ml-sm"></util-a>
                    </util-flex>
                </util-column>
            </util-row>
        </util-search-form>
    </util-card>
    

    上面的標籤會轉換成 Ng Zorro 原生的 html 標籤.

    <nz-card class="searchForm" [nzBorderless]="true">
        <form nz-form="">
            <div nz-row="" [nzGutter]="24">
                <div nz-col="" [nzLg]="8" [nzMd]="12" [nzSm]="24" [nzXl]="8" [nzXs]="24" [nzXXl]="6">
                    <nz-form-item>
                        <nz-form-label style="width:120px">{{'identity.application.code'|i18n}}</nz-form-label>
                        <nz-form-control>
                            <nz-input-group [nzSuffix]="tmp_code">
                                <input #code="" #model_code="ngModel" name="code" nz-input="" [(ngModel)]="queryParam.code" />
                            </nz-input-group>
                            <ng-template #tmp_code="">
                                <i (click)="model_code.reset()" *ngIf="model_code.value" class="ant-input-clear-icon"
                                    nz-icon="" nzTheme="fill" nzType="close-circle">
                                </i>
                            </ng-template>
                        </nz-form-control>
                    </nz-form-item>
                </div>
                <div nz-col="" [nzLg]="8" [nzMd]="12" [nzSm]="24" [nzXl]="8" [nzXs]="24" [nzXXl]="6">
                    <nz-form-item>
                        <nz-form-label style="width:120px">{{'identity.application.name'|i18n}}</nz-form-label>
                        <nz-form-control>
                            <nz-input-group [nzSuffix]="tmp_name">
                                <input #model_name="ngModel" #name="" name="name" nz-input="" [(ngModel)]="queryParam.name" />
                            </nz-input-group>
                            <ng-template #tmp_name="">
                                <i (click)="model_name.reset()" *ngIf="model_name.value" class="ant-input-clear-icon"
                                    nz-icon="" nzTheme="fill" nzType="close-circle">
                                </i>
                            </ng-template>
                        </nz-form-control>
                    </nz-form-item>
                </div>
                <div nz-col="" [nzLg]="8" [nzMd]="12" [nzSm]="24" [nzXl]="8" [nzXs]="24" [nzXXl]="6">
                    <nz-form-item>
                        <nz-form-label style="width:120px">{{'identity.application.enabled'|i18n}}</nz-form-label>
                        <nz-form-control>
                            <nz-select #enabled="" #x_enabled="xSelectExtend" name="enabled" x-select-extend="" [(ngModel)]="queryParam.enabled">
                                <nz-option [nzLabel]="'util.defaultOptionText'|i18n"></nz-option>
                                <ng-container *ngIf="!x_enabled.isGroup">
                                    <nz-option *ngFor="let item of x_enabled.options" [nzDisabled]="item.disabled" 
                                        [nzLabel]="item.text|i18n" [nzValue]="item.value">
                                    </nz-option>
                                </ng-container>
                                <ng-container *ngIf="x_enabled.isGroup">
                                    <nz-option-group *ngFor="let group of x_enabled.optionGroups" [nzLabel]="group.text|i18n">
                                        <nz-option *ngFor="let item of group.value" [nzDisabled]="item.disabled" 
                                            [nzLabel]="item.text|i18n" [nzValue]="item.value">
                                        </nz-option>
                                    </nz-option-group>
                                </ng-container>
                            </nz-select>
                        </nz-form-control>
                    </nz-form-item>
                </div>
                <div *ngIf="expand" nz-col="" [nzLg]="8" [nzMd]="12" [nzSm]="24" [nzXl]="8" [nzXs]="24" [nzXXl]="6">
                    <nz-form-item>
                        <nz-form-label style="width:120px">{{'identity.application.remark'|i18n}}</nz-form-label>
                        <nz-form-control>
                            <nz-input-group [nzSuffix]="tmp_remark">
                                <input #model_remark="ngModel" #remark="" name="remark" nz-input="" [(ngModel)]="queryParam.remark" />
                            </nz-input-group>
                            <ng-template #tmp_remark="">
                                <i (click)="model_remark.reset()" *ngIf="model_remark.value" class="ant-input-clear-icon"
                                    nz-icon="" nzTheme="fill" nzType="close-circle">
                                </i>
                            </ng-template>
                        </nz-form-control>
                    </nz-form-item>
                </div>
                <div *ngIf="expand" nz-col="" [nzLg]="8" [nzMd]="12" [nzSm]="24" [nzXl]="8" [nzXs]="24" [nzXXl]="6">
                    <nz-form-item>
                        <nz-form-label style="width:120px">{{'util.beginCreationTime'|i18n}}</nz-form-label>
                        <nz-form-control>
                            <nz-range-picker #begin_creation_time="" #x_begin_creation_time="xRangePickerExtend" 
                                name="begin_creation_time" x-range-picker-extend="" 
                                [(beginDate)]="queryParam.beginCreationTime" [(endDate)]="queryParam.endCreationTime" 
                                [(ngModel)]="x_begin_creation_time.rangeDates">
                            </nz-range-picker>
                        </nz-form-control>
                    </nz-form-item>
                </div>
                <div *ngIf="expand" nz-col="" [nzLg]="8" [nzMd]="12" [nzSm]="24" [nzXl]="8" [nzXs]="24" [nzXXl]="6">
                    <nz-form-item>
                        <nz-form-label style="width:120px">{{'util.beginLastModificationTime'|i18n}}</nz-form-label>
                        <nz-form-control>
                            <nz-range-picker #begin_last_modification_time="" #x_begin_last_modification_time="xRangePickerExtend" 
                                name="begin_last_modification_time" x-range-picker-extend="" 
                                [(beginDate)]="queryParam.beginLastModificationTime" [(endDate)]="queryParam.endLastModificationTime" 
                                [(ngModel)]="x_begin_last_modification_time.rangeDates">
                            </nz-range-picker>
                        </nz-form-control>
                    </nz-form-item>
                </div>            
                <div class="mb-md" nz-col="" [nzLg]="{span:expand?24:24}" [nzMd]="{span:expand?24:12}" [nzSm]="24" [nzXl]="{span:expand?24:24}" 
                    [nzXs]="24" [nzXXl]="{span:expand?12:6}">
                    <div nz-flex="" nzAlign="center" nzGap="small" nzJustify="flex-end">
                        <button #btnRefresh="" (click)="refresh(btnRefresh)" nz-button="" type="button">
                            <i nz-icon="" nzType="sync"></i>
                            {{'util.reset'|i18n}}
                        </button>
                        <button #btnQuery="" (click)="query(btnQuery)" nz-button="" nzType="primary" type="button">
                            <i nz-icon="" nzType="search"></i>
                            {{'util.query'|i18n}}
                        </button>
                        <a (click)="expand=!expand" class="ml-sm">
                            {{expand?('util.collapse'|i18n):('util.expand'|i18n)}}
                            <i nz-icon="" [nzType]="expand?'up':'down'"></i>
                        </a>
                    </div>
                </div>
            </div>
        </form>
    </nz-card>
    

    <util-search-form> 是 Util UI 的查詢表單標籤.

    查詢表單支持響應式,並將按鈕區域始終放置在最後一行的右側.

    label-width 是一個擴展的範圍設置屬性, 爲每個表單組件的 <nz-form-label> 設置 style="width:120px" 樣式, 避免了分別設置每個組件的寬度.

    Ng Zorro 表單組件由 <nz-form-item> , <nz-form-label> , <nz-form-control> 組合而成.

    <util-input> 和 <util-select> 設置了 label-text , 這是一個擴展屬性,它會激活 <nz-form-item> 結構的自動創建.

    <util-input> 是文本框, 除了爲它自動創建 <nz-form-item> 結構, 還會添加清除內容的功能.

    Util UI 大多常用組件的顯示文本會自動添加 i18n 管道, 比如 'identity.application.code'|i18n ,用於支持多語言.

    從前面的示例可以看到 Util UI 可以大幅提升 html 標籤的書寫效率, 降低維護成本.

  • 易用

    Util 對常用功能進行了高度封裝, 並提供簡單易用的 API.

    易用性是 Util UI 封裝的關鍵目標,也是 Util UI 存在的意義.

    本文後續將以最近更新的一個關鍵功能 - 表格設置, 演示易用性.

  • 強類型提示

    Util UI 提供的標籤使用 TagHelper 技術封裝, 支持強類型提示.

    如果你使用 Vs Code 開發, Util UI 標籤提示信息大致與 Ng Zorro Vs Code 插件提示效果相當.

    Vs Code 的標籤提示信息並不精準, 包含很多與 html 相關的屬性, 比如 aria- 打頭的屬性就佔了幾屏, 這降低了代碼提示的作用.

    如果使用 Vs 開發, 甚至安裝了 Resharper , 代碼提示就能達到最佳效果.

  • 持續更新和改進

    Util UI 不僅僅是對 Ng Zorro 功能的簡單包裝, 更提供了常用功能的擴展.

    Util UI 擴展功能來自之前使用其它 UI 框架的經驗, 另外收集項目開發時的實際需求,並加以整理,以滿足使用 Util UI 的項目.

    Util 團隊傾聽開發人員的心聲, 並持續改進, 從而更好的滿足項目需求.

Util 應用框架 UI 的封裝實現方式

  • 使用 .cshtml 替代 .html 頁面.

    .cshtml 是 .Net 提供的一種高級 html 封裝技術.

    Util 創造性的將 .cshtml 引入 Angular 應用開發.

    Util 將 cshtml 頁面作爲 html 抽象層, 用來隱藏 html 的複雜性.

    Ng Zorro 組件庫定義了大量的 Angular 組件.

    使用 Angular 組件, 就是在 html 頁面中書寫自定義的標籤.

    Util 應用框架使用 TagHelper 對 Ng Zorro 標籤進行封裝, 以提供更加簡潔的用法.

    TagHelper 是一種 .Net 標籤, 在 .cshtml 文件中使用.

    雖然 TagHelper 標籤看上去也是一些自定義標籤 , 但它們不是 Angular 組件.

    Util 會在開發階段將 .cshtml 文件轉換成 html.

  • 使用 Angular 指令進行擴展.

    Ng Zorro 組件庫與 EasyUI 這樣的組件庫具有顯著差異.

    Ng Zorro 組件庫提供的 API 具有粒度細, 擴展性強的特點.

    Ng Zorro 組件的很多功能並不內置於組件中,而是通過 Demo 的形式告訴你怎麼使用.

    這爲你提供了很大的靈活性和自由.

    但也意味着,如果你不加封裝,直接在項目中複製使用, 就會造成大量的冗餘代碼, 降低項目的可維護性.

    要擴展 Ng Zorro 組件, 僅使用 TagHelper 封裝 html 是不夠的, 還需要找到編寫腳本的地方.

    封裝和擴展 Ng Zorro 組件, 通常有兩種方式.

    • 一種方式是創建新的 Angular 組件對原始組件進行包裝.

      使用組件包裝, 可以提供更加易用的 Api.

      不過這種封裝方式也有一些缺陷.

      • 新組件的 API 與原始組件可能不同, 增加了學習成本.

      • 由於需要將原始組件的 API 暴露出來 , 導致更多的冗餘代碼.

      • 擴展性降低.

        對於表格這樣複雜的組件, html 結構相當複雜, 使用組件包裝通常不會保留原有的 html 結構.

        擴展點完全由新組件控制, 從而降低擴展性.

    • 另一種方式是使用 Angular 指令對原始組件進行擴展.

      Angular 指令使用起來就像標籤上的屬性一樣.

      使用 Angular 指令進行擴展, 最大優勢是保留原始組件的全部用法, 不會降低其擴展性.

      當然指令封裝方式也帶來了新的挑戰,那就是 html 標籤會更加複雜.

      Util UI 使用 Angular 指令進行封裝擴展, 並使用 TagHelper 標籤來隱藏 html 的複雜度.

  • Lambda表達式支持

    在 .cshtml 文件中使用 TagHelper 標籤, 你可以直接設置標籤上的屬性.

    不過 , 如果使用 .Net 開發 API 後端, 並創建了 DTO 對象, 你可以將 DTO 屬性直接綁定到標籤上.

    下面演示查詢表單組件如何使用Lambda表達式綁定 DTO 屬性.

    DTO 代碼如下:

    /// <summary>
    /// 應用程序查詢參數
    /// </summary>
    public class ApplicationQuery : QueryParameter {
        /// <summary>
        /// 應用程序編碼
        ///</summary>
        [Description( "identity.application.code" )]
        public string Code { get; set; }
        /// <summary>
        /// 應用程序名稱
        ///</summary>
        [Description( "identity.application.name" )]
        public string Name { get; set; }
        /// <summary>
        /// 啓用
        ///</summary>
        [Description( "identity.application.enabled" )]
        public bool? Enabled { get; set; }
        /// <summary>
        /// 備註
        ///</summary>
        [Description( "identity.application.remark" )]
        public string Remark { get; set; }
        /// <summary>
        /// 起始創建時間
        /// </summary>
        [Display( Name = "util.beginCreationTime" )]
        public DateTime? BeginCreationTime { get; set; }
        /// <summary>
        /// 結束創建時間
        /// </summary>
        [Display( Name = "util.endCreationTime" )]
        public DateTime? EndCreationTime { get; set; }
        /// <summary>
        /// 起始最後修改時間
        /// </summary>
        [Display( Name = "util.beginLastModificationTime" )]
        public DateTime? BeginLastModificationTime { get; set; }
        /// <summary>
        /// 結束最後修改時間
        /// </summary>
        [Display( Name = "util.endLastModificationTime" )]
        public DateTime? EndLastModificationTime { get; set; }
    }
    

    .cshtml 代碼如下:

    @model ApplicationQuery
    
    <util-card borderless="true" class="searchForm">
        <util-search-form label-width="120">
            <util-row gutter="24">
                <util-column>
                    <util-input for="Code" />
                </util-column>
                <util-column>
                    <util-input for="Name" />
                </util-column>
                <util-column>
                    <util-select for="Enabled" />
                </util-column>
                <util-column>
                    <util-input for="Remark" />
                </util-column>
                <util-column>
                    <util-range-picker for-begin="BeginCreationTime" for-end="EndCreationTime" />
                </util-column>
                <util-column>
                    <util-range-picker for-begin="BeginLastModificationTime" for-end="EndLastModificationTime" />
                </util-column>
                <util-column class="mb-md" md="24">
                    <util-flex justify="FlexEnd" align="Center" gap="Small">
                        <util-button id="btnRefresh" icon="Sync" on-click="refresh(btnRefresh)" text-reset="true"></util-button>
                        <util-button id="btnQuery" type="Primary" icon="Search" on-click="query(btnQuery)" text-query="true"></util-button>
                        <util-button icon="CheckSquare" on-click="container.masterToggle()" text-select-all="true" ng-if="!container.isMasterChecked()"></util-button>
                        <util-button icon="CloseSquare" on-click="container.masterToggle()" text-deselect-all="true" ng-if="container.isMasterChecked()"></util-button>
                        <util-a is-search="true" class="ml-sm"></util-a>
                    </util-flex>
                </util-column>
            </util-row>
        </util-search-form>
    </util-card>
    

    Lambda表達式會讀取 DTO 對象的元數據, 並自動設置常用屬性, 從而再次大幅提升生產力.

Util 應用框架 UI 的組成

  • Util.Ui.NgZorro

    Util.Ui.NgZorro 類庫包含 Ng Zorro TagHelper 標籤, 目前已封裝官方正式發佈的全部組件.

  • Util.Ui.NgAlain

    Util.Ui.NgAlain 類庫包含 Ng Alain 部分組件 TagHelper 標籤.

  • util-angular

    util-angular 是一個 typescript 腳本庫, 包含 Ng Zorro 擴展指令和常用操作 Helper.

Util 應用框架 UI 最新進展

Util 應用框架 UI 最近進行了全面改進,並取得了重大突破.

最大的進展有2點, 一是開發機制的改進, 二是增加了表格設置功能.

  • 開發機制改進

    • 架構缺陷

      Util 應用框架將 .cshtml 文件引入 Angular 已有相當長的年頭.

      由於這種非主流的用法並沒有微軟官方的支持,所以一直存在相當多的問題.

      • 最主要的影響是導致開發階段運行緩慢.

        之前的開發流程, Angular 組件在開發階段直接訪問 cshtml 頁面,所以開發階段必須使用 Angular JIT 模式, 它比 Angular AOT 模式要慢一些.

        cshtml 在第一次訪問時, 尚未創建緩存 , 會比較慢.

        Angular 應用啓動時,將訪問根模塊引用的所有頁面, 所以啓動時會產生相當的卡頓.

        這個問題通過 Angular 延遲加載模塊得到緩解.

        如果項目比較大,包含數十個業務模塊, 將每個業務模塊創建爲延遲加載模塊.

        當應用啓動時, 並不會訪問所有頁面, 只有請求了某個業務模塊的功能, 纔會訪問該模塊包含的 cshtml 頁面.

        不過從 Angular 13 開始, Angular 移除了傳統的視圖引擎, 導致上述開發方式無法使用延遲加載模塊.

        這意味着所有業務模塊在開發階段必須在根模塊中引用.

        Angular 應用啓動後將訪問所有 cshtml 頁面, 這顯然是不可接受的.

        一種可行的解決辦法是使用微前端方案.

        微前端架構將業務模塊分離到不同的項目從而減少應用啓動時間.

        一些較大的項目和團隊使用微前端架構是合適的.

        但微前端架構具有複雜性, 使用微前端架構代替延遲加載模塊則非常牽強.

        這是 Util 團隊進行全面改造的根本原因.

      • 另一個影響是項目結構比較複雜.

        Util 採用的項目結構最早來自 .Net Core Angular 項目模板, 並加以修改.

        Angular 應用被放在 ClientApp 目錄中.

        .cshtml 文件則被放在 Pages 目錄中.

        這導致組件與模板的對應關係比較複雜.

    • 改進方案

      很多時候, 解決問題最重要是思路的轉變.

      之前的架構缺陷主要來自在開發階段讓 Angular 組件直接請求 cshtml 頁面,從而與原生 Angular 應用產生差別.

      不過, Util 使用 cshtml 僅限於開發階段, 發佈之後實際上與 cshtml 沒有任何關係.

      cshtml 的作用只是幫助生成 html 而已.

      現代化開發一個重要的功能是熱更新, 比如 Angular 應用, 它會持續監視你的相關文件.

      當你編輯完 .ts 或 .html 文件時, 瀏覽器就會自動刷新.

      如果我們監視所有 .cshtml 文件,並在保存 cshtml 文件時自動生成對應的 html 文件,就能從根本上解決問題.

      由於只需要處理保存的 cshtml 文件, 生成 html 的速度將非常迅速.

      當 html 生成完成, 後續流程則與原生 angular 應用相同, 從而解決引入 cshtml 相關的所有缺陷.

      現在, 編輯並保存 .cshtml 文件, 瀏覽器就會自動刷新, 與原生 Angular 應用相比, 大致慢幾百毫秒, 通常可以忽略不計.

      項目結構複雜的問題則很好解決, 將 .cshtml 與 Angular 組件放在一起即可.

      這與原生 Angular 應用相似, 只需修改 .cshtml 生成 html 文件的路徑規則.

      一直以來, Util UI的架構比較臃腫, 只能在 Vs 中開發.

      但現在前端基本都使用 Vs Code.

      最新 UI 架構與原生 Angular 應用差別很小, 同樣適合使用 Vs Code 開發.

      下面是使用 Vs Code 打開的項目結構.

  • 表格設置

    表格是業務系統的基石.

    我們收集了一些項目上使用 Ng Zorro 表格的反饋意見.

    • 當表格列較多時,如果不進行寬度設置, 則會顯示得很畸形.

      要解決這個問題, 需要設置表格 nzScroll 屬性的 x 值.

      nzScroll 的 x 可以讓表格產生橫向的滾動條, 從而將表格內容拉伸.

      不過這個值應該設置成多少合適, 則是一門學問.

      通常需要計算表格中有多少列,每列大致佔多少寬度, nzScroll.x 的值大致是這些寬度之和.

      手工計算寬度費時費力, 最好是能自動計算.

    • 另一個問題是凍結表格頭, 並讓表格在一定高度滾動.

      通過設置 nzScroll 屬性的 y 值可以做到這一點.

      不過設置 nzScroll.y 也是一門學問, 因爲不同屏幕大小可能需要設置不同的值,在開發階段很難固定.

      一些公司使用某些方法計算以達到自適應高度,不過大多針對比較固定的頁面佈局,且相對簡單.

      更好的辦法是讓用戶在運行時根據自己的要求動態更新.

    • 除了表格的總寬度, 每個列的寬度設置也是一個頭痛的問題.

      列寬大多與內容相關, 在開發階段設置固定列寬, 當內容超過固定寬度就會出現換行,影響美觀.

      如果在開發階段設置一個默認寬度, 並在運行時可由用戶修改就能解決問題.

      當然最好能支持拖動表頭修改列寬, 則更爲方便.

    • 自定義列是很多項目的必備功能.

      當表格列非常多, 用戶希望只顯示其中感興趣的一部分列, 並能修改列的顯示順序.

      Ng Zorro 支持自定義列功能, 不過使用起來比較複雜.

      當你啓用了自定義列, 用來固定左右側的 nzLeft 和 nzRight 就變得不那麼利索.

      列與列之間經常會出現一些縫隙或對不齊的現象, Ng Zorro 官方文檔給出了一些調整建議, 不過也是非常麻煩.

    • 諸如表格批量編輯,表格行編輯, 樹形異步加載等需求都是很早之前就已經擴展支持, 就不在此一一列出.

    下面介紹 Util UI 表格設置功能.

    先來一個表格設置的效果圖.

    可以看到, 它確實解決了前面提到的棘手問題.

    如何開啓表格設置功能?

    表格標籤示例代碼.

    @*表格*@
    <util-table id="tb" key="identity_operation" enable-table-settings="true"
                show-checkbox="true" show-line-number="true" 
                url="operation" query-param="queryParam" sort="SortId">
        <util-td for="Name"></util-td>
        <util-td for="Uri"></util-td>
        <util-td for="IsBase" sort="false"></util-td>
        <util-td for="Remark"></util-td>
        <util-td for="Enabled">
            <util-tag color-type="GeekBlue" ng-if="row.enabled" text-enabled="true"></util-tag>
            <util-tag color-type="Red" ng-if="!row.enabled" text-not-enabled="true"></util-tag>
        </util-td>
        <util-td for="CreationTime"></util-td>
        <util-td for="LastModificationTime"></util-td>
        <util-td title-operation="true">
            <util-a on-click="openDetailDialog(row)" text-detail="true"></util-a>
            <util-container acl="operation.update">
                <util-divider type="Vertical"></util-divider>
                <util-a on-click="openEditDrawer(row)" text-update="true"></util-a>
            </util-container>
            <util-container acl="operation.delete">
                <util-divider type="Vertical"></util-divider>
                <util-a danger="true" on-click="delete(row.id)" text-delete="true"></util-a>
            </util-container>
        </util-td>
    </util-table>
    

    要開啓表格設置功能, 只需要在 <util-table> 標籤設置 enable-table-settings 屬性爲 true.

    你可能要問, 需要編寫 ts 腳本代碼嗎?

    不用 !!!

    如果你看過 Ng Zorro 官方自定義列的示例, 知道需要將一個 NzCustomColumn[] 對象傳入 <nz-table>的 nzCustomColumn 屬性.

    那麼, Util UI 的自定義列功能是否使用 Ng Zorro 官方的實現呢?

    下面來看看生成的 html , 答案就會揭曉.

    <nz-table #tb="" #x_tb="xTableExtend" (nzPageIndexChange)="x_tb.pageIndexChange($event)"
        (nzPageSizeChange)="x_tb.pageSizeChange($event)" order="SortId" url="operation" x-table-extend=""
        [(nzPageIndex)]="x_tb.queryParam.page" [(nzPageSize)]="x_tb.queryParam.pageSize" [(queryParam)]="queryParam"
        [nzBordered]="ts_tb.bordered" [nzCustomColumn]="ts_tb.columns" [nzData]="x_tb.dataSource"
        [nzFrontPagination]="false" [nzLoading]="x_tb.loading" [nzPageSizeOptions]="x_tb.pageSizeOptions"
        [nzScroll]="ts_tb.scroll" [nzShowQuickJumper]="true" [nzShowSizeChanger]="true" [nzShowTotal]="total_tb"
        [nzSize]="ts_tb.size" [nzTotal]="x_tb.total">
        <thead>
            <tr>
                <th (nzCheckedChange)="x_tb.masterToggle()" nzCellControl="util.checkbox"
                    [nzChecked]="x_tb.isMasterChecked()" [nzDisabled]="!x_tb.dataSource.length"
                    [nzIndeterminate]="x_tb.isMasterIndeterminate()" [nzLeft]="ts_tb.isLeft('util.checkbox')"
                    [nzRight]="ts_tb.isRight('util.checkbox')" [nzShowCheckbox]="true"
                    [titleAlign]="ts_tb.getTitleAlign('util.checkbox')">
                </th>
                <th nzCellControl="util.lineNumber" [nzLeft]="ts_tb.isLeft('util.lineNumber')"
                    [nzRight]="ts_tb.isRight('util.lineNumber')" [titleAlign]="ts_tb.getTitleAlign('util.lineNumber')">
                    {{'util.lineNumber'|i18n}}
                </th>
                <th (nzResizeEnd)="ts_tb.handleResize($event,'identity.operation.name')"
                    (nzSortOrderChange)="x_tb.sortChange('name',$event)" nz-resizable="" nzBounds="window"
                    nzCellControl="identity.operation.name" nzPreview="" [nzLeft]="ts_tb.isLeft('identity.operation.name')"
                    [nzRight]="ts_tb.isRight('identity.operation.name')" [nzShowSort]="true" [nzSortFn]="true"
                    [titleAlign]="ts_tb.getTitleAlign('identity.operation.name')">
                    {{'identity.operation.name'|i18n}}
                    <nz-resize-handle nzDirection="right"></nz-resize-handle>
                </th>
                <th (nzResizeEnd)="ts_tb.handleResize($event,'identity.operation.uri')"
                    (nzSortOrderChange)="x_tb.sortChange('uri',$event)" nz-resizable="" nzBounds="window"
                    nzCellControl="identity.operation.uri" nzPreview="" [nzLeft]="ts_tb.isLeft('identity.operation.uri')"
                    [nzRight]="ts_tb.isRight('identity.operation.uri')" [nzShowSort]="true" [nzSortFn]="true"
                    [titleAlign]="ts_tb.getTitleAlign('identity.operation.uri')">
                    {{'identity.operation.uri'|i18n}}
                    <nz-resize-handle nzDirection="right"></nz-resize-handle>
                </th>
                <th (nzResizeEnd)="ts_tb.handleResize($event,'identity.operation.isBase')" nz-resizable="" nzBounds="window"
                    nzCellControl="identity.operation.isBase" nzPreview=""
                    [nzLeft]="ts_tb.isLeft('identity.operation.isBase')"
                    [nzRight]="ts_tb.isRight('identity.operation.isBase')"
                    [titleAlign]="ts_tb.getTitleAlign('identity.operation.isBase')">
                    {{'identity.operation.isBase'|i18n}}
                    <nz-resize-handle nzDirection="right"></nz-resize-handle>
                </th>
                <th (nzResizeEnd)="ts_tb.handleResize($event,'identity.operation.remark')"
                    (nzSortOrderChange)="x_tb.sortChange('remark',$event)" nz-resizable="" nzBounds="window"
                    nzCellControl="identity.operation.remark" nzPreview=""
                    [nzLeft]="ts_tb.isLeft('identity.operation.remark')"
                    [nzRight]="ts_tb.isRight('identity.operation.remark')" [nzShowSort]="true" [nzSortFn]="true"
                    [titleAlign]="ts_tb.getTitleAlign('identity.operation.remark')">
                    {{'identity.operation.remark'|i18n}}
                    <nz-resize-handle nzDirection="right"></nz-resize-handle>
                </th>
                <th (nzResizeEnd)="ts_tb.handleResize($event,'identity.operation.enabled')"
                    (nzSortOrderChange)="x_tb.sortChange('enabled',$event)" nz-resizable="" nzBounds="window"
                    nzCellControl="identity.operation.enabled" nzPreview=""
                    [nzLeft]="ts_tb.isLeft('identity.operation.enabled')"
                    [nzRight]="ts_tb.isRight('identity.operation.enabled')" [nzShowSort]="true" [nzSortFn]="true"
                    [titleAlign]="ts_tb.getTitleAlign('identity.operation.enabled')">
                    {{'identity.operation.enabled'|i18n}}
                    <nz-resize-handle nzDirection="right"></nz-resize-handle>
                </th>
                <th (nzResizeEnd)="ts_tb.handleResize($event,'util.creationTime')"
                    (nzSortOrderChange)="x_tb.sortChange('creationTime',$event)" nz-resizable="" nzBounds="window"
                    nzCellControl="util.creationTime" nzPreview="" [nzLeft]="ts_tb.isLeft('util.creationTime')"
                    [nzRight]="ts_tb.isRight('util.creationTime')" [nzShowSort]="true" [nzSortFn]="true"
                    [titleAlign]="ts_tb.getTitleAlign('util.creationTime')">{{'util.creationTime'|i18n}}
                    <nz-resize-handle nzDirection="right"></nz-resize-handle>
                </th>
                <th (nzResizeEnd)="ts_tb.handleResize($event,'util.lastModificationTime')"
                    (nzSortOrderChange)="x_tb.sortChange('lastModificationTime',$event)" nz-resizable="" nzBounds="window"
                    nzCellControl="util.lastModificationTime" nzPreview=""
                    [nzLeft]="ts_tb.isLeft('util.lastModificationTime')"
                    [nzRight]="ts_tb.isRight('util.lastModificationTime')" [nzShowSort]="true" [nzSortFn]="true"
                    [titleAlign]="ts_tb.getTitleAlign('util.lastModificationTime')">
                    {{'util.lastModificationTime'|i18n}}
                    <nz-resize-handle nzDirection="right"></nz-resize-handle>
                </th>
                <th (nzResizeEnd)="ts_tb.handleResize($event,'util.operation')" nz-resizable="" nzBounds="window"
                    nzCellControl="util.operation" nzPreview="" [nzLeft]="ts_tb.isLeft('util.operation')"
                    [nzRight]="ts_tb.isRight('util.operation')" [titleAlign]="ts_tb.getTitleAlign('util.operation')">
                    {{'util.operation'|i18n}}
                    <nz-resize-handle nzDirection="right"></nz-resize-handle>
                </th>
            </tr>
        </thead>
        <tbody>
            <tr *ngFor="let row of x_tb.dataSource;index as index">
                <td (click)="$event.stopPropagation()" (nzCheckedChange)="x_tb.toggle(row)" nzCellControl="util.checkbox"
                    [nzAlign]="ts_tb.getAlign('util.checkbox')" [nzChecked]="x_tb.isChecked(row)"
                    [nzLeft]="ts_tb.isLeft('util.checkbox')" [nzRight]="ts_tb.isRight('util.checkbox')"
                    [nzShowCheckbox]="true">
                </td>
                <td nzCellControl="util.lineNumber" [nzAlign]="ts_tb.getAlign('util.lineNumber')"
                    [nzLeft]="ts_tb.isLeft('util.lineNumber')" [nzRight]="ts_tb.isRight('util.lineNumber')">
                    {{row.lineNumber}}
                </td>
                <td nzCellControl="identity.operation.name" [nzAlign]="ts_tb.getAlign('identity.operation.name')"
                    [nzEllipsis]="ts_tb.getEllipsis('identity.operation.name')"
                    [nzLeft]="ts_tb.isLeft('identity.operation.name')" [nzRight]="ts_tb.isRight('identity.operation.name')">
                    {{row.name}}
                </td>
                <td nzCellControl="identity.operation.uri" [nzAlign]="ts_tb.getAlign('identity.operation.uri')"
                    [nzEllipsis]="ts_tb.getEllipsis('identity.operation.uri')"
                    [nzLeft]="ts_tb.isLeft('identity.operation.uri')" [nzRight]="ts_tb.isRight('identity.operation.uri')">
                    {{row.uri}}
                </td>
                <td nzCellControl="identity.operation.isBase" [nzAlign]="ts_tb.getAlign('identity.operation.isBase')"
                    [nzEllipsis]="ts_tb.getEllipsis('identity.operation.isBase')"
                    [nzLeft]="ts_tb.isLeft('identity.operation.isBase')"
                    [nzRight]="ts_tb.isRight('identity.operation.isBase')">
                    <i *ngIf="!row.isBase" nz-icon nzType="close"></i>
                    <i *ngIf="row.isBase" nz-icon nzType="check"></i>
                </td>
                <td nzCellControl="identity.operation.remark" [nzAlign]="ts_tb.getAlign('identity.operation.remark')"
                    [nzEllipsis]="ts_tb.getEllipsis('identity.operation.remark')"
                    [nzLeft]="ts_tb.isLeft('identity.operation.remark')"
                    [nzRight]="ts_tb.isRight('identity.operation.remark')">
                    {{row.remark}}
                </td>
                <td nzCellControl="identity.operation.enabled" [nzAlign]="ts_tb.getAlign('identity.operation.enabled')"
                    [nzEllipsis]="ts_tb.getEllipsis('identity.operation.enabled')"
                    [nzLeft]="ts_tb.isLeft('identity.operation.enabled')"
                    [nzRight]="ts_tb.isRight('identity.operation.enabled')">
                    <nz-tag *ngIf="row.enabled" nzColor="geekblue">{{'util.enabled'|i18n}}</nz-tag>
                    <nz-tag *ngIf="!row.enabled" nzColor="red">{{'util.notEnabled'|i18n}}</nz-tag>
                </td>
                <td nzCellControl="util.creationTime" [nzAlign]="ts_tb.getAlign('util.creationTime')"
                    [nzEllipsis]="ts_tb.getEllipsis('util.creationTime')" [nzLeft]="ts_tb.isLeft('util.creationTime')"
                    [nzRight]="ts_tb.isRight('util.creationTime')">
                    {{row.creationTime|date:'yyyy-MM-dd HH:mm'}}
                </td>
                <td nzCellControl="util.lastModificationTime" [nzAlign]="ts_tb.getAlign('util.lastModificationTime')"
                    [nzEllipsis]="ts_tb.getEllipsis('util.lastModificationTime')"
                    [nzLeft]="ts_tb.isLeft('util.lastModificationTime')"
                    [nzRight]="ts_tb.isRight('util.lastModificationTime')">
                    {{row.lastModificationTime|date:'yyyy-MM-dd HH:mm'}}
                </td>
                <td nzCellControl="util.operation" [nzAlign]="ts_tb.getAlign('util.operation')"
                    [nzEllipsis]="ts_tb.getEllipsis('util.operation')" [nzLeft]="ts_tb.isLeft('util.operation')"
                    [nzRight]="ts_tb.isRight('util.operation')">
                    <a (click)="openDetailDialog(row)">{{'util.detail'|i18n}}</a>
                    <ng-container *aclIf="'operation.update'">
                        <nz-divider nzType="vertical"></nz-divider>
                        <a (click)="openEditDrawer(row)">{{'util.update'|i18n}}</a>
                    </ng-container>
                    <ng-container *aclIf="'operation.delete'">
                        <nz-divider nzType="vertical"></nz-divider>
                        <a (click)="delete(row.id)" class="ant-btn-dangerous">{{'util.delete'|i18n}}</a>
                    </ng-container>
                </td>
            </tr>
        </tbody>
    </nz-table>
    <ng-template #total_tb="" let-range="range" let-total="">
        {{ 'util.tableTotalTemplate'|i18n:{start:range[0],end:range[1],total:total} }}
    </ng-template>
    <x-table-settings #ts_tb=""
        key="identity_operation" [enableFixedColumn]="true"
        [initColumns]="[{'title':'util.checkbox','width':x_tb.config.table.checkboxWidth,'align':'left'},
        {'title':'util.lineNumber','width':x_tb.config.table.lineNumberWidth,'align':'left'},
        {'title':'identity.operation.name'},{'title':'identity.operation.uri'},
        {'title':'identity.operation.isBase'},{'title':'identity.operation.remark'},
        {'title':'identity.operation.enabled'},{'title':'util.creationTime'},
        {'title':'util.lastModificationTime'},{'title':'util.operation'}]">
    </x-table-settings>
    

    觀察 <nz-table> 標籤, 可以發現 [nzCustomColumn]="ts_tb.columns" , 說明確實使用的是 Ng Zorro 官方提供的自定義列功能.

    生成的 html 比較複雜, enable-table-settings 除了開啓自定義列外,還會啓用拖動列寬等功能.

    前面提到, Util Ui 提供的標籤可以壓縮 3-10 倍的 html 代碼量 , 從這裏可以看出, 絕非信口雌黃.

    <x-table-settings> 是由 util-angular 腳本庫提供的表格設置組件.

    <x-table-settings> 的 initColumns 屬性設置了一個列信息數組, 將列集合傳入表格設置組件.

    <x-table-settings> 組件經過系列工序, 輸出 Ng Zorro 需要的自定義列信息.

    所以, 無需手工編寫任何 ts 腳本代碼, 即可完成相關功能.

    可以看到, TagHelper 不僅可以封裝 html 複雜度,甚至能爲你生成一些簡單的 js 對象.

    要打開表格設置對話框, 需要一個按鈕.

    .cshtml 代碼如下.

    show-table-settings 用於顯示錶格設置對話框, 傳入表格的引用變量名 tb.

    <util-a show-table-settings="tb"></util-a>
    

    生成的 html 如下.

    <a (click)="ts_tb.show()" nz-tooltip="" [nzTooltipTitle]="'util.tableSettings'|i18n">
        <i nz-icon="" nzType="setting"></i>
    </a>
    

    Util UI 的擴展指令和組件具有一些約定的命名.

    表格組件的引用變量名爲 tb , 對應的表格設置組件則爲 ts_tb .

    表格設置組件提供了一個 show() 函數, 調用該函數即可打開表格設置窗口.

總結

本文分享了 Util 應用框架 UI 最近的突破與進展.

Util 應用框架 UI 最新架構已經穩定, 可以放心使用.

一些開發人員問到使用教程, 嗯, 這是個傷心事, Util 應用框架一直是心傳口授模式, 確實沒有.

不過 Util 也在考慮突破原有的使用羣體, 面向更大的範圍傳播.

使用教程和文檔已經在路上, 歡迎大家使用 , 我們將以更快的速度提供.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章