Util 應用框架 UI 全新升級

何镇汐發表於2024-04-28

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 也在考慮突破原有的使用群體, 面向更大的範圍傳播.

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

相關文章