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 也在考虑突破原有的使用群体, 面向更大的范围传播.

使用教程和文档已经在路上, 欢迎大家使用 , 我们将以更快的速度提供.

标签:游戏攻略