feat(form): 增加表单栅格布局模块

新增 FormModules::grid 与 gridColumn,用统一方法生成 1 到 4 列的标准表单栅格,避免各业务表单重复拼接 layui 栅格类名。

在表单渲染壳层中注入标准样式,统一栅格列宽、字段间距、单行说明省略和自动补全下拉层样式,保证弹窗与页面模式展示一致。

补充 FormBuilderTest 覆盖可复用栅格布局、nowrap 说明文案和标准样式输出,防止后续表单布局能力回退。
This commit is contained in:
邹景立 2026-05-21 00:15:34 +08:00
parent 217b1c9ea9
commit 875c3a16d7
3 changed files with 108 additions and 0 deletions

View File

@ -13,6 +13,48 @@ use think\admin\builder\form\FormNode;
*/ */
class FormModules class FormModules
{ {
/**
* 标准表单栅格容器.
*/
public static function grid(FormNode $parent, int $columns = 2, string|array $class = '', int $space = 15): FormNode
{
$columns = self::normalizeGridColumns($columns);
$space = max(0, min(30, $space));
$node = $parent->div()->class([
'layui-row',
"layui-col-space{$space}",
'ta-form-grid',
"ta-form-grid-{$columns}",
]);
if ($class !== '' && $class !== []) {
$node->class($class);
}
return $node;
}
/**
* 标准表单栅格列.
*/
public static function gridColumn(FormNode $parent, int $columns = 2, string|array $class = ''): FormNode
{
$columns = self::normalizeGridColumns($columns);
$span = match ($columns) {
1 => 12,
2 => 6,
3 => 4,
default => 3,
};
$node = $parent->div()->class([
"layui-col-xs{$span}",
'ta-form-grid-col',
]);
if ($class !== '' && $class !== []) {
$node->class($class);
}
return $node;
}
/** /**
* @param array<string, mixed> $config * @param array<string, mixed> $config
*/ */
@ -84,4 +126,9 @@ class FormModules
{ {
return htmlentities($content, ENT_QUOTES, 'UTF-8'); return htmlentities($content, ENT_QUOTES, 'UTF-8');
} }
private static function normalizeGridColumns(int $columns): int
{
return max(1, min(4, $columns));
}
} }

View File

@ -39,6 +39,7 @@ class FormShellRenderer
$html = sprintf('<form %s>', $context->attrs($attrs)); $html = sprintf('<form %s>', $context->attrs($attrs));
$html .= "\n\t" . sprintf('<div %s>', $context->attrs($bodyAttrs)); $html .= "\n\t" . sprintf('<div %s>', $context->attrs($bodyAttrs));
$html .= "\n\t\t" . $this->renderStandardStyle();
if (count($content) > 0) { if (count($content) > 0) {
$html .= $context->renderChildren($content); $html .= $context->renderChildren($content);
@ -76,6 +77,7 @@ class FormShellRenderer
$header = (new PageHeaderRenderer())->render(strval($schema['title'] ?? ''), $headerButtons); $header = (new PageHeaderRenderer())->render(strval($schema['title'] ?? ''), $headerButtons);
$form = sprintf('<form %s>', $context->attrs($attrs)); $form = sprintf('<form %s>', $context->attrs($attrs));
$form .= "\n\t\t\t\t" . sprintf('<div %s>', $context->attrs($bodyAttrs)); $form .= "\n\t\t\t\t" . sprintf('<div %s>', $context->attrs($bodyAttrs));
$form .= "\n\t\t\t\t\t" . $this->renderStandardStyle();
if (count($content) > 0) { if (count($content) > 0) {
$form .= "\n\t\t\t\t\t" . $context->renderChildren($content); $form .= "\n\t\t\t\t\t" . $context->renderChildren($content);
@ -116,4 +118,30 @@ class FormShellRenderer
$html .= "\n\t" . '</div>'; $html .= "\n\t" . '</div>';
return $html . "\n</div>"; return $html . "\n</div>";
} }
private function renderStandardStyle(): string
{
return <<<'HTML'
<style data-form-builder-standard-style>
.ta-form-grid{display:flex;flex-wrap:wrap;align-items:flex-start}
.ta-form-grid>.ta-form-grid-col{box-sizing:border-box}
.ta-form-grid-1>.ta-form-grid-col{width:100%!important;max-width:100%}
.ta-form-grid-2>.ta-form-grid-col{width:50%!important;max-width:50%}
.ta-form-grid-3>.ta-form-grid-col{width:33.333333%!important;max-width:33.333333%}
.ta-form-grid-4>.ta-form-grid-col{width:25%!important;max-width:25%}
.ta-form-grid>.ta-form-grid-col .layui-form-item{margin-bottom:14px}
.ta-form-grid-compact>.ta-form-grid-col{padding-top:4px!important;padding-bottom:4px!important}
.ta-form-grid-compact>.ta-form-grid-col .layui-form-item{margin-bottom:6px}
.ta-form-grid-compact>.ta-form-grid-col .help-block{margin-top:2px;line-height:1.4}
.ta-form-nowrap section>div:first-child p,
.ta-form-nowrap .ta-form-grid-col .help-label,
.ta-form-nowrap .ta-form-grid-col .help-block,
.ta-form-grid.ta-form-nowrap .help-label,
.ta-form-grid.ta-form-nowrap .help-block{display:block;max-width:100%;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}
.ta-form-autocompleter{top:58px;z-index:19892000;min-width:100%;border-radius:0 0 6px 6px;box-shadow:0 8px 24px rgba(15,23,42,.14)}
.ta-form-autocompleter .autocompleter-list{max-height:220px;overflow-y:auto;background:#fff}
.ta-form-autocompleter .autocompleter-item{overflow:hidden;white-space:nowrap;text-overflow:ellipsis}
</style>
HTML;
}
} }

View File

@ -25,6 +25,7 @@ use think\admin\Controller;
use think\admin\builder\form\FormBuilder; use think\admin\builder\form\FormBuilder;
use think\admin\builder\form\FormComponents; use think\admin\builder\form\FormComponents;
use think\admin\builder\form\FormChoiceField; use think\admin\builder\form\FormChoiceField;
use think\admin\builder\form\module\FormModules;
use think\admin\builder\form\FormSelectField; use think\admin\builder\form\FormSelectField;
use think\admin\builder\form\FormTextField; use think\admin\builder\form\FormTextField;
use think\admin\builder\form\FormUploadField; use think\admin\builder\form\FormUploadField;
@ -348,6 +349,38 @@ class FormBuilderTest extends TestCase
$this->assertStringContainsString('data-builder-modules=', $html); $this->assertStringContainsString('data-builder-modules=', $html);
} }
public function testFormModulesCanRenderReusableGridLayout(): void
{
$builder = $this->newBuilder();
$builder->define(function ($form) {
$form->class('ta-form-nowrap');
FormModules::section($form, [
'title' => '布局',
'description' => '描述信息保持单行显示。',
], function ($section) {
$grid = FormModules::grid($section, 3, 'profile-grid');
FormModules::gridColumn($grid, 3)->fields(function ($fields) {
$fields->text('nickname', '用户名称', 'Nickname', true, '单行说明');
});
FormModules::gridColumn($grid, 3)->fields(function ($fields) {
$fields->text('email', '联系邮箱', 'Email');
});
FormModules::gridColumn($grid, 3)->fields(function ($fields) {
$fields->text('phone', '联系电话', 'Phone');
});
});
})->build();
$html = $this->invokePrivate($builder, '_buildFormModal');
$this->assertStringContainsString('data-form-builder-standard-style', $html);
$this->assertStringContainsString('ta-form-nowrap', $html);
$this->assertStringContainsString('layui-row layui-col-space15 ta-form-grid ta-form-grid-3 profile-grid', $html);
$this->assertStringContainsString('layui-col-xs4 ta-form-grid-col', $html);
$this->assertStringContainsString('width:33.333333%!important', $html);
$this->assertStringContainsString('white-space:nowrap', $html);
}
public function testPageModeCanRenderTitleAndDefaultPadding(): void public function testPageModeCanRenderTitleAndDefaultPadding(): void
{ {
$builder = $this->newBuilder('form', 'page'); $builder = $this->newBuilder('form', 'page');