Util 應用框架 UI 全新升級

来源:https://www.cnblogs.com/xiadao521/p/18163513
-Advertisement-
Play Games

Util UI 已經開發多年, 併在多家公司的項目使用. 不過一直以來, Util UI 存在一些缺陷, 始終未能解決. 最近幾個月, Util 團隊下定決心, 終於徹底解決了所有已知缺陷. Util 應用框架 UI 介紹 Util 應用框架 UI 建立在 Angular , Ng-Zorro, N ...


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

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

Util應用框架交流群: 24791014 歡迎轉載 何鎮汐的技術博客 微信掃描二維碼支持Util
您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 原創研發uniapp+vue3+pinia2跨三端仿微信app聊天模板Uniapp-Wechat。 uni-vue3-wchat基於uni-app+vue3+pinia2+uni-ui+uv-ui等技術跨端仿製微信App界面聊天項目,支持編譯到H5+小程式端+App端。實現編輯框多行消息/emoj混 ...
  • 1.現象 當在vue中列印對象的時候會發現有一些屬性或者全部屬性都是顯示的...,點擊展開後才能看到真正的值是什麼. 2.原因 因為在vue中對象都是用了代理重寫了get,由於get重寫也就導致了瀏覽器不能直接獲取到具體的值,因此才會在列印的時候為...,手動點擊展開才顯示具體的值 3.想看具體的值 ...
  • 項目結構 在開發Chrome插件時,以下幾個文件的作用如下: manifest.json:這是Chrome插件的清單文件,用於配置插件的基本信息、許可權、頁面跳轉等。其中包括插件的名稱、版本號、圖標、後臺腳本、瀏覽器動作等信息。 background.js:這是Chrome插件的後臺腳本文件,用於處理 ...
  • DevTools 非常強大除了常用的查看元素,進行斷點調試或許還有些你不知道的小技巧,小功能。如可以快速的重新發送請求,快速選擇元素,在控制臺中使用npm庫等,讓你能夠更加高效的進行開發。不定時更新~ ...
  • 大家好,我是 Java陳序員。 在日常的工作生活中,我們經常會遇到應付各類強制要求轉發朋友圈的行為,或者是朋友圈集贊的行為。 今天,給大家介紹一個工具,可以幫助你生成朋友圈轉發截圖。 關註微信公眾號:【Java陳序員】,獲取開源項目分享、AI副業分享、超200本經典電腦電子書籍等。 項目介紹 We ...
  • 拖放功能,即將一個元素從一個區域,通過拖拽,放置到另一個區域。常見的應用是將文件或圖片從一個區域,拖放到另一個區域。中文常常把這表述成拖拽,實際上拖拽的描述並不准確,應該叫拖放,因為drag事件和drop事件是成對使用的,即拖拽和放置。 drag在拖拽動作發生時觸發,攜帶被拖拽元素的信息,drop在 ...
  • 1. computed(計算屬性)和方法有什麼區別? 計算屬性本質上是包含 getter 和 setter 的方法 當獲取計算屬性時,實際上是在調用計算屬性的 getter 方法。vue 會收集計算屬性的依賴,並緩存計算屬性的返回結果。只有當依賴變化後才會重新進行計算。 方法沒有緩存,每次調用方法都 ...
  • 大家好,我是 Java陳序員。 今天,給大家介紹一個簡潔、開源的中後臺管理模板項目。 關註微信公眾號:【Java陳序員】,獲取開源項目分享、AI副業分享、超200本經典電腦電子書籍等。 項目介紹 nova-admin —— 一個基於Vue3、Vite5、Typescript、Naive UI, 簡 ...
一周排行
    -Advertisement-
    Play Games
  • .Net8.0 Blazor Hybird 桌面端 (WPF/Winform) 實測可以完整運行在 win7sp1/win10/win11. 如果用其他工具打包,還可以運行在mac/linux下, 傳送門BlazorHybrid 發佈為無依賴包方式 安裝 WebView2Runtime 1.57 M ...
  • 目錄前言PostgreSql安裝測試額外Nuget安裝Person.cs模擬運行Navicate連postgresql解決方案Garnet為什麼要選擇Garnet而不是RedisRedis不再開源Windows版的Redis是由微軟維護的Windows Redis版本老舊,後續可能不再更新Garne ...
  • C#TMS系統代碼-聯表報表學習 領導被裁了之後很快就有人上任了,幾乎是無縫銜接,很難讓我不想到這早就決定好了。我的職責沒有任何變化。感受下來這個系統封裝程度很高,我只要會調用方法就行。這個系統交付之後不會有太多問題,更多應該是做小需求,有大的開發任務應該也是第二期的事,嗯?怎麼感覺我變成運維了?而 ...
  • 我在隨筆《EAV模型(實體-屬性-值)的設計和低代碼的處理方案(1)》中介紹了一些基本的EAV模型設計知識和基於Winform場景下低代碼(或者說無代碼)的一些實現思路,在本篇隨筆中,我們來分析一下這種針對通用業務,且只需定義就能構建業務模塊存儲和界面的解決方案,其中的數據查詢處理的操作。 ...
  • 對某個遠程伺服器啟用和設置NTP服務(Windows系統) 打開註冊表 HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\W32Time\TimeProviders\NtpServer 將 Enabled 的值設置為 1,這將啟用NTP伺服器功 ...
  • title: Django信號與擴展:深入理解與實踐 date: 2024/5/15 22:40:52 updated: 2024/5/15 22:40:52 categories: 後端開發 tags: Django 信號 松耦合 觀察者 擴展 安全 性能 第一部分:Django信號基礎 Djan ...
  • 使用xadmin2遇到的問題&解決 環境配置: 使用的模塊版本: 關聯的包 Django 3.2.15 mysqlclient 2.2.4 xadmin 2.0.1 django-crispy-forms >= 1.6.0 django-import-export >= 0.5.1 django-r ...
  • 今天我打算整點兒不一樣的內容,通過之前學習的TransformerMap和LazyMap鏈,想搞點不一樣的,所以我關註了另外一條鏈DefaultedMap鏈,主要調用鏈為: 調用鏈詳細描述: ObjectInputStream.readObject() DefaultedMap.readObject ...
  • 後端應用級開發者該如何擁抱 AI GC?就是在這樣的一個大的浪潮下,我們的傳統的應用級開發者。我們該如何選擇職業或者是如何去快速轉型,跟上這樣的一個行業的一個浪潮? 0 AI金字塔模型 越往上它的整個難度就是職業機會也好,或者說是整個的這個運作也好,它的難度會越大,然後越往下機會就會越多,所以這是一 ...
  • @Autowired是Spring框架提供的註解,@Resource是Java EE 5規範提供的註解。 @Autowired預設按照類型自動裝配,而@Resource預設按照名稱自動裝配。 @Autowired支持@Qualifier註解來指定裝配哪一個具有相同類型的bean,而@Resourc... ...