Skip to content

表单

基础表单

在 zin 中使用基础表单可以通过 formBaseform 部件实现,他们用于构建 HTML 表单 <form> 元素的基本结构。 form 相比较 formBase 提供了 itemsfields 等属性来通过配置定义表单,而且支持通过 layout 来自定义布局。

表单控件

在 zin 中提供了大量表单控件,这些控件可以直接通过 zin 部件的方式进行声明调用,下面列举常用表单控件。

在 zin 中还提供了一个通用表单控件部件 control 用于动态定义表单控件。

表单布局

布局设置

zin 提供了多种布局方式,可以在 form() 内通过 layout 属性来设置表单的布局方式,包括:

  • horz:水平布局,表单经典布局,表单项标签在左侧,右侧表单控件。
  • grid:网格布局,表单新的布局,表单项标签在上方,下方为表单控件,表单项可以水平排列。
  • normal:普通布局,表单项从上往下排列布局,每个表单项标签在上方,下方为表单控件。

表单布局组件

要实现表单布局,还需要搭配如下部件实现:

  • 表单组 formGroup:用于实现表单布局的常用单元,包括表单项标签和控件,适用于所有表单布局。
  • 表单行 formRow:用于构建水平布局,使子部件以內联的形式排列于一行之中,它一般与表单组部件formGroup 一起使用。

表单面板

表单面板是对表单的进一步封装,提供了额外的头部标题和自定义按钮等设置,在 zin 中提供了如下部件用于使用表单面板:

formPanelformGridPanel 继承自 面板 panel,可以同时使用 panelform 的属性进行功能定制。

表单字段配置

字段定义

字段和字段列表的定义参考文档 字段

禅道字段列表定义约定

在禅道中,各个模块的字段列表定义位置约定如下:

  • 模块的基础字段列表定义在 module/<MODULE_NAME>/ui/common.field.php 文件中;
  • 模块具体页面的字段列表定义在 module/<MODULE_NAME>/ui/<METHOD_NAME>.field.php 文件中;
  • 无需在视图中手动导入上述字段定义文件,禅道会自动导入;

下面为一个实例:

php
<?php
namespace zin;
global $lang;

$fields = defineFieldList('bug');

$fields->field('product')
    ->required()
    ->items(data('products'))
    ->value(data('bug.productID'));

$fields->field('branch')
    ->items(data('branchs'))
    ->value(data('bug.branch'));

$fields->field('deadline')
    ->control('datePicker');

$fields->field('title')
    ->width('full');

$fields->field('color')
    ->control('color');

$fields->field('keywords');

$fields->field('case')->control('hidden')->value(data('bug.caseID'));
$fields->field('caseVersion')->control('hidden')->value(data('bug.version'));
$fields->field('result')->control('hidden')->value(data('bug.runID'));
$fields->field('testtask')->control('hidden')->value(data('bug.testtask'));

$fields = defineFieldList('bug.kanban');

$fields->field('region')
    ->label($lang->kanbancard->region)
    ->items(data('regionPairs'))
    ->value(data('regionID'));
$fields->field('lane')
    ->label($lang->kanbancard->lane)
    ->items(data('lanePairs'))
    ->value(data('laneID'));
php
<?php
namespace zin;

global $lang;

$fields = defineFieldList('bug.create', 'bug', '!branch,color');

$fields->field('product')
    ->hidden(data('product.shadow'))
    ->control('inputGroup')
    ->items(false)
    ->itemBegin('product')->type('picker')->items(data('products'))->value(data('bug.productID'))->itemEnd()
    ->item((data('product.type') !== 'normal' && isset(data('products')[data('bug.productID')])) ? field('branch')->type('picker')->boxClass('flex-none')->width('100px')->name('branch')->items(data('branches'))->value(data('bug.branch')) : null);

$fields->field('title')
    ->width('full')
    ->control('colorInput', array('colorValue' => data('bug.color')));
php
<?php
declare(strict_types=1);
namespace zin;

$fields = useFields('bug.create');

if(!empty($executionType) && $executionType == 'kanban') $fields->merge('bug.kanban');

jsVar('bug',                   $bug);
jsVar('moduleID',              $bug->moduleID);
jsVar('tab',                   $this->app->tab);
jsVar('createRelease',         $lang->release->create);
jsVar('refresh',               $lang->refreshIcon);
jsVar('projectExecutionPairs', $projectExecutionPairs);

$autoLoad = array();
$autoLoad['product']   = 'product,module,openedBuild,execution,project,story,task,assignedTo';
$autoLoad['branch']    = 'module,openedBuild,execution,project,story,task,assignedTo';
$autoLoad['module']    = 'assignedTo,story';
$autoLoad['project']   = 'openedBuild,execution,story,task,assignedTo';
$autoLoad['execution'] = 'openedBuild,story,task,assignedTo';
$autoLoad['region']    = 'lane';

formGridPanel
(
    set::title($lang->bug->create),
    set::fields($fields),
    set::loadUrl($loadUrl),
    set::autoLoad($autoLoad),
    on::click('#allBuilds',             'loadAllBuilds'),
    on::click('#allUsers',              'loadAllUsers'),
    on::click('#refreshExecutionBuild', 'refreshExecutionBuild'),
    on::click('#refreshProductBuild',   'refreshProductBuild'),
);

使用字段

通过 useFields() 方法来引用名称的方式来使用字段列表中的字段,例如:

php
/** 使用字段(省略定义字段的代码)。 */
$fields = useFields('bug', $isKanban ? 'bug.kanban' : null, '!color,region');

/** 获取字段列表中的所有字段。 */
$names = $fields->names(); // array('title', 'product', 'branch', 'lane')

修改字段

字段列表实例 fieldList 上提供了一些方法可以修改字段列表,例如:

php
/** 修改 title 字段。 */
$fields->field('title')->label('标题')->required();

/** 根据条件移除 branch 字段。 */
if($noBranch)
{
    $fields->remove('branch');
}

/** 增加新的字段。 */
$fields->field('newField')->label('新字段');

批量定义字段标签数据

通过字段列表实例方法 setLabelData() 可以批量定义字段标签数据,例如:

php
$labelData = array(
    'title'    => '标题',
    'product'  => '产品',
    'branch'   => '分支',
    'lane'     => '泳道',
    'newField' => '新字段',
);
$fields->setLabelData($labelData);

批量定义字段默认值数据

通过字段列表实例方法 setValueData() 可以批量定义字段默认值数据,例如:

php
$valueData = array(
    'title'    => '测试',
    'product'  => '1',
    'branch'   => '2',
    'lane'     => '3',
    'newField' => '4',
);
$fields->setValueData($valueData);

在表单中使用字段列表

通过 fields 属性可以将字段列表应用到表单相关部件中,所有支持 fields 属性的部件包括:

示例:

php
/** 使用字段(省略定义字段的代码)。 */
$fields = useFields('bug', $isKanban ? 'bug.kanban' : null, '!color,region');

/** 修改 title 字段。 */
$fields->field('title')->label('标题')->required();

/** 根据条件移除 branch 字段。 */
if($noBranch)
{
    $fields->remove('branch');
}

/** 增加新的字段。 */
$fields->field('newField')->label('新字段');

/** 批量定义字段标签数据。 */
$labelData = array(
    'title'    => '标题',
    'product'  => '产品',
    'branch'   => '分支',
    'lane'     => '泳道',
    'newField' => '新字段',
);
$fields->setLabelData($labelData);

/** 批量定义字段默认值数据。 */
$valueData = array(
    'title'    => '测试',
    'product'  => '1',
    'branch'   => '2',
    'lane'     => '3',
    'newField' => '4',
);
$fields->setValueData($valueData);

/** 定义网格表单面板。 */
formGirdPanel
(
    /* 通过 fields 定义表单项。 */
    set::fields($fields) // 设置字段列表
);

表单字段顺序

表单字段顺序默认取决于字段列表中的顺序,但也可以通过 fieldList 实例方法 orders() 来调整字段顺序,下面分别进行介绍。

修改字段列表定义顺序

在表单字段列表 fieldList 实例上提供了如下方法来调整字段顺序:

php
/** 将字段移动到字段列表的开头。 */
moveToBegin(string $names): fieldList

/** 将字段移动到字段列表的末尾。 */
moveToEnd(string $names): fieldList

/** 将字段移动到指定字段的前方位置。 */
moveBefore(string $names, string $beforeName): fieldList

/** 将字段移动到指定字段的后方位置。 */
moveAfter(string $names, string $afterName): fieldList

/** 对字段列表中的字段进行排序。 */
sort(string|array ...$sortNames): fieldList

提示

  • 可以通过 , 分隔多个字段名称,也可以通过多个参数来指定多个字段名称。
  • 其中排序字段可以以 '$BEGIN''$END' 分别表示移动到字段列表的开始或结尾位置。

示例:

php
/** 定义名称为 `bug` 的字段列表。 */
$fields = defineFieldList('bug');

/** 在字段列表上定义字段。 */
$fields->field('title');
$fields->field('product')->label('产品')->required();
$fields->field('branch');
$fields->field('bug');
$fields->field('color');
$fields->field('type');
$fields->field('keywords');

/* 将 color 字段移动到字段列表的开头: */
$fields->moveToBegin('color');
// 或 $fields->field('color')->moveToBegin();
// 调整后的顺序为:color, title, product, branch, bug, type, keywords

/* 将 title 字段移动到字段列表的末尾: */
$fields->moveToEnd('title,branch');
// 调整后的顺序为:color, product, bug, type, keywords, title, branch

/* 将字段移到指定字段后面: */
$fields->moveAfter('title,branch', 'color');
// 调整后的顺序为:color, title, branch, product, bug, type, keywords

/* 将字段移到指定字段前面: */
$fields->moveBefore('title,branch', 'color');
// 调整后的顺序为:title, branch, color, product, bug, type, keywords

/* 按规则对字段进行排序: */
$fields->sort('color,title,branch');
// 将 title 移到 color 后面,然后将 branch 移到 title 后面
// 调整后的顺序为:color, title, branch, product, bug, type, keywords

/* 指定多个规则对字段进行排序: */
$fields->sort('color,title,branch', '$BEGIN,type,product');
// 将 title 移到 color 后面,然后将 branch 移到 title 后面
// 将 type 移到字段列表的开始,然后将 product 移到 type 后面
// 调整后的顺序为:type, product, color, title, branch, bug, keywords

定义字段导出顺序

默认情况下表单中的字段会安装字段列表中显示的顺序进行显示,但有时需要调整表单中字段的显示顺序,这时可以通过 fieldList 实例方法 orders() 来定义字段导出顺序,如果需要为完整模式定义字段导出顺序,则可以通过 fullModeOrders() 方法来定义。相关方法定义如下:

php
/** 定义字段导出顺序。 */
orders(string|array ...$names): fieldList

/** 定义完整模式字段导出顺序。 */
fullModeOrders(string|array ...$names): fieldList

提示

  • 可以通过 , 分隔多个字段名称,也可以通过多个参数来指定多个字段名称。
  • 其中排序字段可以以 '$BEGIN''$END' 分别表示移动到字段列表的开始或结尾位置。

示例:

php
/** 定义名称为 `bug` 的字段列表。 */
$fields = defineFieldList('bug');

/** 在字段列表上定义字段。 */
$fields->field('title');
$fields->field('product')->label('产品')->required();
$fields->field('branch');
$fields->field('bug');
$fields->field('color');

/** 定义字段导出顺序。 */
$fields->orders('branch,title', '$BEGIN,color,bug');
// 将 title 移到 branch 字段后面,然后将 color 移到列表开始,接着将 bug 移到 color 后面
// 默认模式下的顺序为:color, bug, product, branch, title,

/** 定义完整模式字段导出顺序。 */
$fields->fullModeOrders('branch,title,bug,color,product');
// 依次按照 branch, title, bug, color, product 的顺序导出字段
// 完整模式下的顺序为:branch, title, bug, color, product

表单字段渲染

表单字段渲染一般通过 control 属性来指定,详细用法参考 字段 ➡️ 设置字段表单控件。下面介绍主要用法。

渲染指定类型的表单控件

control 属性设置为 zin 部件名称即可,如果要设置渲染的部件其他属性,应该将 control 属性设置为一个数组,数组内通过 control 属性指定部件名称,其他属性作为部件实际渲染的属性。

php
/** 使用字段(省略定义字段的代码)。 */
$fields = useFields('bug', $isKanban ? 'bug.kanban' : null, '!color,region');

/* 字段控件渲染为普通输入框。 */
$fields->field('title')->control('input');

/* 等同于: */
$fields->field('title')->control(array('control' => 'input'));

/* 省略 control 设置。 */
$fields->field('title');

/* 字段控件渲染为下拉选择器。 */
$fields->field('type')->control('picker', array('multiple' => true, 'items' => $items));

/* 字段控件渲染为下拉选择器。 */
$fields->field('type')->control('picker', array('multiple' => true, 'items' => $items));

/* 通过 `controlBegin` 和 `controlEnd` 链式设置控件属性。 */
$fields->field('title')
    ->controlBegin('picker')
    ->multiple()
    ->items($items)
    ->controlEnd();

直接指定渲染的内容

有时一些特殊内容没有合适的控件,可以通过 control 属性直接指定渲染的内容,或者指定为回调函数,回调函数的返回值作为渲染的内容,例如:

直接设置渲染的内容:

php
$fields->field('title')->control(div('hello world')); // 渲染为 div。

通过回调函数来设置渲染的内容,回调函数第一个参数为当前字段上的所有属性:

php
$buildTitleField = function($props)
{
    return div
    (
        'hello world',
        data('lang.zentaoPMS'),
        json_encode($props)
    );
};

$fields->field('title')->control($buildTitleField);

表单联动

通过 autoLoad 属性设置联动规则

表单联动为当表单某个字段变更时,需要更新其他字段的选项,在 zin 中可以通过中相关表单布局中的 autoLoad 属性来设置联动规则。所有支持 autoLoad 属性的部件包括:

联动规则通过一个关联数组定义,数组键名为触发联动的字段名或选择器,键值为联动触发时要更新的表单字段,通过 , 分隔多个需要更新的字段,下面为一个实例:

ts
/* 定义联动规则数组。 */
$autoLoad = array();

/* 当 [name="product"] 表单控件变更时,更新 product,module,openedBuild,execution,project 表单控件。 */
$autoLoad['product'] = 'product,module,openedBuild,execution,project';

/* 当 [name="module"] 表单控件变更时,更新 assignedTo,story 表单控件。 */
$autoLoad['module'] = 'assignedTo,story';

/* 当 [name="region"] 表单控件变更时,更新 lane 表单控件。 */
$autoLoad['[name="region"]'] = 'lane';

/* 声明网格表单面板部件。 */
formGridPanel
(
    /* 通过 fields 定义表单项。 */
    set::fields(useFields('bug')), // 设置字段列表

    /* 定义表单字段默认值获取对象。 */
    set::data($bug),

    /* 定义表单字段标签获取对象。 */
    set::labelData($lang->bug),

    /* 定义联动规则。 */
    set::autoLoad($autoLoad)
);

通过 fieldListautoLoad() 方法设置联动规则

通过字段列表实例方法 autoLoad() 可以设置联动规则,例如:

php
/** 使用字段(省略定义字段的代码)。 */
$fields = useFields('bug', $isKanban ? 'bug.kanban' : null, '!color,region');

/** 通过字段列表实例方法 `autoLoad()` 可以设置联动规则。 */
$fields->autoLoad('product', 'product,module,openedBuild,execution,project')
    ->autoLoad(array('module' => 'assignedTo,story', '[name="region"]' => 'lane'));

表单提交

表单默认启用了异步提交的功能,即表单提交后,不会刷新页面,而是通过 ajax 请求后台处理表单数据,然后根据后台返回的结果进行失败或成功的相应处理。

示例 中的 form 部件声明:

php
form
(
    ...
);
html
<form class="form load-indicator form-grid form-ajax" id="zin10" action="/project-create.html" method="post">
    ...
    <div class="form-row">
        <div class="toolbar form-actions form-group gap-4 no-label">
            <button class="toolbar-item btn primary" type="submit"><span class="text">保存</span></button>
            <a class="toolbar-item btn btn-default" type="button" href=""><span class="text">返回</span></a>
        </div>
    </div>
    <script>(function(){$(() => zui.create("ajaxForm","#zin10",[]));}())</script>
</form>

通过上面的示例代码可以看出,在表单的最后输出了 ZUI JS 组件 的绑定脚本 (function(){$(() => zui.create("ajaxForm","#zin10",[]));}()),将表单绑定为 ajaxForm 组件,由该组件实现了表单的异步交互。

提示

  1. 如需要停用表单的异步提交功能,可将 target 属性设置为 非ajax 值。
  2. 设置表单 action 属性,指定表单提交的目标地址。

Ajax 表单用法

在禅道中所有表单通过 ajax 的形式向后端提交,后端根据情况通过 json 的形式向前端返回结果。下面以示例的方式分别说明提交失败和成功时的结果定义。

提交失败的处理结果

通过 message 字段指定失败信息:

json
{
    "result": "fail",
    "message": "项目名称不能为空"
}

通常我们需要告诉用户页面哪个字段验证失败,这时可以为 message 指定一个对象表明所有存在错误的字段:

json
{
    "result": "fail",
    "message":
    {
        "name": "项目名称不能为空",
        "begin": "开始日期不能为空",
        "end": ["结束日期不能为空", "结束日期不能小于开始日期"]
    }
}

失败结果的 message 数据的键为表单控件的 name 属性值,值为错误提示信息。

提交成功的处理结果

当表单提交成功时,可以通过各种字段来指定后续操作,下面为一个完整的例子,实际情况中按需定义:

json
{
    "result": "success",

    /* 此消息会在界面进行短暂提示 */
    "message": "创建成功",

    /* 提交成功后要在当前应用跳转的页面,该属性可以为其他类型的值,参考下文 */
    "load": "project-browse-1.html",

    /* 提交成功后关闭对话框,该属性可以为其他类型的值,参考下文 */
    "closeModal": true,

    /* 要执行的回调函数,该属性可以为其他类型的值,参考下文 */
    "callback": "loadCurrentPage();"
}

load 属性的所有形式

类型说明
string当前页面通过一个具体的 url 更新,通常用于表单界面跳回列表页。
true则直接更新当前应用页面,通常适用于对话框表单。
'table'直接更新当前应用页面中的列表,确保界面用到了 dtable。
'modal'更新当前显示的对话框
'login'跳转到登录页面
{url: string, selector: string | string[]}更新当前应用页面中的匹配指定选择器的内容,例如 {url: 'project-browse-1.html', selector: '#featureBar>*'}
{confirm: string, confirmed: string | {url: string, selector: string | string[]}, canceled: string | {url: string, selector: string | string[]}}先弹出一个确认对话框,确认后再执行更新操作,没有确认执行另一个更新操作,例如 {confirm: '要返回列表页面吗?', confirmed: {url: 'project-browse-1.html', selector: '#featureBar>*'}, canceled: 'task-view-1.html'}
OpenUrlOptions通过 load 参数指定一个 openUrl 方法所需的选项,详情参考文档 禅道页面导航 → 万能的 openUrl

closeModal 属性的所有形式

类型说明
true关闭最后一个打开的对话框,通常用于通过对话框打开的表单,指定次属性可以在表单提交成功后关闭当前对话框。
string通过一个字符串指定要关闭的对话框的 ID,例如 closeModal: "myModal"

callback 属性的所有形式

类型说明
string指定一个函数名,例如 callback: "loadCurrentPage",或者指定调用代码,例如 callback: 'loadCurrentPage("#featureBar>*");'
{name: string, params: unknown[]}通过一个对象分别指定函数名和调用参数,例如 {name: 'loadCurrentPage', params: ['#featureBar>*']}

手动提交

有时需要手动提交数据到服务器,例如用户点击删除按钮,这时需要将所删除的对象 ID 提交到服务器,服务器返回提交结构定义对象,然后根据提交结果执行相应的操作。这时可以使用 $.ajaxSubmit 方法来实现,该方法定义如下:

ts
function $.ajaxSubmit(options: AjaxSubmitOptions): Promise<[result: AjaxFormResult | undefined, error: Error | undefined]>;

该方法需要传入一个参数对象,定义如下:

ts
type AjaxSubmitOptions = {
    /* 提交的目标地址,例如 'project-delete-123.html'。 */
    url: string;

    /* 要提交的数据,例如 {id: 123}。 */
    data?: Record<string, unknown> | FormData;

    /* 提交的方式,默认 'POST'。 */
    method?: string;

    /* 提交的数据类型,默认 'json'。 */
    headers?: HeadersInit;

    /* 目标元素。 */
    element?: HTMLElement;

    /* 提交时要在目标元素上添加的类名。 */
    loadingClass?: string;

    /* 提交前的回调函数,返回 false 取消提交。 */
    beforeSubmit?: (options: AjaxSubmitOptions) => false | void;

    /* 发起请求前的回调函数,可以修改 formData 返回新的 FormData。 */
    beforeSend?: (formData: FormData) => void | FormData;

    /* 提交成功后的回调函数,返回 false 取消后续操作。 */
    onSuccess?: (result: AjaxFormResult) => void | false;

    /* 提交失败后的回调函数,返回 false 取消后续操作。 */
    onFail?: (result: AjaxFormResult) => void | false;

    /* 请求发生错误的回调函数。 */
    onError?: (error: Error, responseText?: string) => void;

    /* 请求完成的回调函数。 */
    onComplete?: (result?: AjaxFormResult, error?: Error) => void;

    /* 当需要显示提示消息的回调函数。 */
    onMessage?: (message: string | Record<string, string | string[]>, result: AjaxFormResult) => void;

    /* 提交成功后要在当前应用跳转的页面或加载选项,此选项会覆盖后端返回的 load 参数。 */
    load?: string | {url?: string, selector?: string | string[]};

    /* 提交成功后关闭对话框,此选项会覆盖后端返回的 closeModal 参数。 */
    closeModal?: boolean | string;

    /* 要执行的回调函数,此选项会覆盖后端返回的 callback 参数。 */
    callback?: string | {name: string, target?: string, params?: unknown[]};
};

该方法会通过 Promise 使用数组返回两个值,第一个值为 AjaxFormResult,第二个为 Error 对象,根据实际情况,这两个值都有可能为 undefined

下面为一个简单例子:

html
<button type="button" class="btn" onclick="deleteProject(123)">删除项目</button>

<script>
function deleteProject(id)
{
    $.ajaxSubmit({
        /* 请求地址。 */
        url: 'project-delete.html',

        /* 请求参数。 */
        data: {id: id},

        /* 请求成功后关闭对话框。 */
        closeModal: true,

        /* 请求成功后自动跳转到项目列表页面。 */
        load: 'project-browse-1.html',
    });
}
</script>

$.ajaxSubmit 还可以通过添加类名 .ajax-submit 在元素上触发,当元素被点击时通过获取 data-* 作为初始化选项调用 $.ajaxSubmit 方法,例如上例可以改写为:

html
<button type="button" class="btn ajax-submit" data-url="project-delete.html" data-data='{"id": 123}' data-close-modal="true" data-load="project-browse-1.html">删除项目</button>

综合示例

下面为禅道创建项目集页面(program-create的 zin 实现代码(部分处理逻辑已经被省略):

php
<?php
namespace zin;

$parentID          = $parentProgram->id ?? 0;
$currency          = $parentID ? $parentProgram->budgetUnit : $config->project->defaultCurrency;
$aclList           = $parentProgram ? $lang->program->subAclList : $lang->program->aclList;
$budgetPlaceholder = $parentProgram ? $lang->program->parentBudget . zget($lang->project->currencySymbol, $parentProgram->budgetUnit) . $budgetLeft : '';
$budgetAvaliable   = !$parentID || $budgetLeft;

jsVar('LONG_TIME', LONG_TIME);
jsVar('lang', ['budgetOverrun' => $lang->project->budgetOverrun, 'currencySymbol' => $lang->project->currencySymbol, 'ignore' => $lang->program->ignore]);
jsVar('weekend', $config->execution->weekend);

set::title($parentID ? $lang->program->children : $lang->program->create);

formPanel
(
    on::change('#parent', 'onParentChange'),
    on::change('#budget', 'onBudgetChange'),
    on::change('#future', 'onFutureChange'),
    on::change('#acl',    'onAclChange'),
    formGroup
    (
        set::width('1/2'),
        set::name('parent'),
        set::label($lang->program->parent),
        set::disabled($parentID),
        set::value($parentID),
        set::items($parents),
    ),
    formGroup
    (
        set::width('1/2'),
        set::name('name'),
        set::strong(true),
        set::label($lang->program->name)
    ),
    formGroup
    (
        set::width('1/4'),
        set::name('PM'),
        set::label($lang->program->PM),
        set::items($pmUsers)
    ),
    formRow
    (
        set::id('budgetRow'),
        formGroup
        (
            set::width('1/2'),
            set::label($lang->program->budget),
            inputGroup
            (
                set::seg(true),
                input
                (
                    set::name('budget'),
                    set::placeholder($budgetPlaceholder),
                    set::disabled(!$budgetAvaliable),
                    set('data-budget-left', $budgetLeft),
                    set('data-currency-symbol', $parentProgram ? zget($lang->project->currencySymbol, $parentProgram->budgetUnit) : NULL),
                ),
                select
                (
                    zui::width('1/3'),
                    set::name('budgetUnit'),
                    set::disabled($parentID || !$budgetAvaliable),
                    set::items($budgetUnitList),
                    set::value($currency)
                )
            )
        ),
        formHidden('budgetUnit', $currency),
        formGroup
        (
            set::name('future'),
            set::value('1'),
            set::disabled(!$budgetAvaliable),
            set::control(['type' => 'checkbox', 'rootClass' => 'ml-4', 'text' => $lang->project->future, 'checked' => !$budgetAvaliable])
        ),
    ),
    formRow
    (
        formGroup
        (
            set::width('1/2'),
            set::label($lang->project->dateRange),
            set::required(true),
            inputGroup
            (
                set::seg(true),
                input
                (
                    set::type('date'),
                    set::name('begin'),
                    set::value(date('Y-m-d')),
                    set::placeholder($lang->project->begin),
                    set::required(true),
                    on::change('computeWorkDays')
                ),
                $lang->project->to,
                input
                (
                    set::type('date'),
                    set::name('end'),
                    set::placeholder($lang->project->end),
                    set::required(true),
                    on::change('outOfDateTip')
                ),
            )
        ),
        formGroup
        (
            set::name('delta'),
            set::class('pl-4'),
            set::control(['type' => 'radioList', 'inline' => true, 'rootClass' => 'ml-4', 'items' => $lang->program->endList]),
            on::change('computeEndDate')
        ),
    ),
    formGroup
    (
        set::name('desc'),
        set::label($lang->program->desc),
        set::control('editor')
    ),
    formHidden('status', 'wait'),
    formGroup
    (
        set::name('acl'),
        set::label($lang->program->acl),
        set::value('private'),
        set::items($aclList),
        set::control('radioList'),
    ),
    formRow
    (
        set::id('whitelistRow'),
        formGroup
        (
            set::width('3/4'),
            set::name('whitelist'),
            set::label($lang->whitelist),
            set::control('select')
        )
    )
);

render();

https://zentao.net