style(menu): 精简菜单编辑表单布局

菜单表单改用通用栅格模块组织层级归属、跳转权限和展示策略,缩短说明文案并开启单行省略,使弹窗内容更紧凑。

将图标选择器合并到菜单图标输入框右侧按钮,保留实时图标预览,减少独立选择器字段占用的空间。

调整链接和权限预览、自动补全下拉样式及图标选择 iframe 打开方式,并补充测试覆盖新布局类名、预览字段和旧文案移除。
This commit is contained in:
邹景立 2026-05-21 00:16:20 +08:00
parent 7e87c0d65a
commit 7408a162b6
2 changed files with 162 additions and 98 deletions

View File

@ -152,98 +152,121 @@ SCRIPT);
->define(function ($form) use ($context) { ->define(function ($form) use ($context) {
$form->action(strval($context['actionUrl'] ?? '')) $form->action(strval($context['actionUrl'] ?? ''))
->attrs(['id' => 'MenuForm', 'data-table-id' => 'MenuTable']) ->attrs(['id' => 'MenuForm', 'data-table-id' => 'MenuTable'])
->class('system-menu-form'); ->class(['system-menu-form', 'ta-form-nowrap']);
$form->html('<style id="SystemMenuFormStyle">' . self::renderFormStyle() . '</style>'); $form->html('<style id="SystemMenuFormStyle">' . self::renderFormStyle() . '</style>');
FormModules::section($form, [ FormModules::section($form, [
'title' => '层级归属', 'title' => '层级归属',
'description' => '先确认上级菜单与菜单名称。分组菜单建议将链接留空或填写 #,业务页面建议直接指向真实页面节点。', 'description' => '确认上级与名称,分组菜单可用 #。',
], function ($section) use ($context) { ], function ($section) use ($context) {
$grid = $section->div()->class('layui-row layui-col-space15'); $grid = FormModules::grid($section, 2, 'system-menu-form-row ta-form-grid-compact', 8);
$left = $grid->div()->class('layui-col-xs12 layui-col-md6'); $parent = FormModules::gridColumn($grid, 2, 'system-menu-basic-col');
$left->fields(function ($fields) use ($context) { $parent->fields(function ($fields) use ($context) {
$fields->select('pid', '上级菜单', 'Parent Menu', true, '请选择上级菜单或顶级菜单。当前仅支持三级菜单结构,叶子菜单不能继续挂载子节点。', self::buildParentOptions(is_array($context['menus'] ?? null) ? $context['menus'] : []), '', ['lay-search' => null]) $fields->select('pid', '上级菜单', 'Parent Menu', true, '选择上级,叶子菜单不可挂载。', self::buildParentOptions(is_array($context['menus'] ?? null) ? $context['menus'] : []), '', ['lay-search' => null]);
->text('title', '菜单名称', 'Menu Title', true, '请填写菜单名称,建议控制在 4-8 个汉字,便于导航栏展示。', null, [
'placeholder' => '请输入菜单名称',
'required-error' => '菜单名称不能为空!',
]);
}); });
$right = $grid->div()->class('layui-col-xs12 layui-col-md6'); $title = FormModules::gridColumn($grid, 2, 'system-menu-basic-col');
FormModules::readonlyField($right, [ $title->fields(function ($fields) {
'title' => '结构约束', $fields->text('title', '菜单名称', 'Menu Title', true, '建议 4-8 个汉字。', null, [
'subtitle' => 'Structure Rules', 'placeholder' => '请输入菜单名称',
'value' => '顶级菜单 / 二级分组 / 三级页面', 'required-error' => '菜单名称不能为空!',
'help' => '系统会在保存时校验父子关系,禁止挂到叶子菜单下,也禁止形成循环层级。', ]);
]); });
FormModules::note($section, '层级最多三级:顶级菜单 / 二级分组 / 三级页面。', 'help-block color-desc system-menu-section-note');
}); });
FormModules::section($form, [ FormModules::section($form, [
'title' => '跳转与权限', 'title' => '跳转与权限',
'description' => '菜单链接建议填写完整页面节点,如 system/user/index。权限节点为空时系统会优先根据菜单链接解析访问控制节点。', 'description' => '设置菜单链接、参数与权限节点。',
], function ($section) { ], function ($section) {
$grid = $section->div()->class('layui-row layui-col-space15'); $grid = FormModules::grid($section, 2, 'system-menu-form-row ta-form-grid-compact', 8);
$route = $grid->div()->class('layui-col-xs12 layui-col-md6'); $route = FormModules::gridColumn($grid, 2);
$route->fields(function ($fields) { $route->fields(function ($fields) {
$fields->text('url', '菜单链接', 'Menu Url', false, '支持系统页面节点、外部地址、插件布局地址或 # 分组节点。保存时会自动标准化链接格式。', null, [ $fields->text('url', '菜单链接', 'Menu Url', false, '页面节点、外链或 # 分组。', null, [
'placeholder' => '例如system/user/index、https://example.com/docs 或 #', 'placeholder' => '例如system/user/index、https://example.com/docs 或 #',
])->text('params', '链接参数', 'Query Params', false, '设置菜单链接的 GET 访问参数,如 tab=user&mode=list。', null, [ ]);
});
$permission = FormModules::gridColumn($grid, 2);
$permission->fields(function ($fields) {
$fields->text('node', '权限节点', 'Permission Node', false, '留空时按菜单链接推导。', null, [
'placeholder' => '例如system/user/index',
]);
});
$params = FormModules::gridColumn($grid, 2);
$params->fields(function ($fields) {
$fields->text('params', '链接参数', 'Query Params', false, 'GET 参数,如 tab=user。', null, [
'placeholder' => '请输入链接参数', 'placeholder' => '请输入链接参数',
]); ]);
}); });
$permission = $grid->div()->class('layui-col-xs12 layui-col-md6'); $target = FormModules::gridColumn($grid, 2);
$permission->fields(function ($fields) { $target->fields(function ($fields) {
$fields->text('node', '权限节点', 'Permission Node', false, '显式指定注释式 RBAC 节点。若留空,将按菜单链接推导默认访问节点。', null, [ $fields->select('target', '打开方式', 'Target Window', true, '选择链接打开方式。', [
'placeholder' => '例如system/user/index',
])->select('target', '打开方式', 'Target Window', true, '设置菜单链接的打开方式。', [
'_self' => '当前窗口', '_self' => '当前窗口',
'_blank' => '新窗口', '_blank' => '新窗口',
]); ]);
}); });
$preview = $section->div()->class('layui-row layui-col-space15'); $preview = FormModules::grid($section, 2, 'system-menu-preview-row ta-form-grid-compact', 8);
$previewUrl = $preview->div()->class('layui-col-xs12 layui-col-md6'); $previewUrl = FormModules::gridColumn($preview, 2, 'system-menu-preview-col');
FormModules::readonlyField($previewUrl, [ FormModules::readonlyField($previewUrl, [
'title' => '标准化链接预览', 'title' => '链接预览',
'subtitle' => 'Normalized Target', 'subtitle' => 'Normalized',
'value' => '', 'value' => '',
'input_attrs' => [ 'input_attrs' => [
'data-menu-url-preview' => null, 'data-menu-url-preview' => null,
'placeholder' => '根据菜单链接与参数自动生成', 'placeholder' => '保存前自动预览',
], ],
'help' => '预览仅用于提示保存结果,实际写库仍以后端校验为准。', 'help' => '预览标准化后的链接。',
]); ]);
$previewNode = $preview->div()->class('layui-col-xs12 layui-col-md6'); $previewNode = FormModules::gridColumn($preview, 2, 'system-menu-preview-col');
FormModules::readonlyField($previewNode, [ FormModules::readonlyField($previewNode, [
'title' => '鉴权节点预览', 'title' => '权限预览',
'subtitle' => 'Resolved Node', 'subtitle' => 'Resolved',
'value' => '', 'value' => '',
'input_attrs' => [ 'input_attrs' => [
'data-menu-node-preview' => null, 'data-menu-node-preview' => null,
'placeholder' => '未显式设置时按菜单链接推导', 'placeholder' => '留空时自动推导',
], ],
'help' => '权限节点必须来自注释式 RBAC 的有效 @auth 节点。', 'help' => '预览 RBAC 访问节点。',
]); ]);
}); });
FormModules::section($form, [ FormModules::section($form, [
'title' => '展示策略', 'title' => '展示策略',
'description' => '统一维护菜单图标、排序权重与启停状态。图标支持 layui 与 iconfont 字体类名。', 'description' => '设置图标、排序和启停状态。',
], function ($section) { ], function ($section) {
$grid = $section->div()->class('layui-row layui-col-space15'); $grid = FormModules::grid($section, 3, 'system-menu-form-row ta-form-grid-compact', 8);
$fields = $grid->div()->class('layui-col-xs12 layui-col-md6'); $iconField = FormModules::gridColumn($grid, 3, 'system-menu-display-col system-menu-icon-col');
$fields->fields(function ($fields) { $iconField->fields(function ($fields) {
$status = $fields->text('icon', '菜单图标', 'Menu Icon', false, '可手动输入图标类名,或通过右侧图标选择器回填。', null, [ $icon = $fields->text('icon', '菜单图标', 'Menu Icon', false, '输入类名或点击选择。', null, [
'placeholder' => '例如layui-icon layui-icon-set-fill', 'placeholder' => '例如layui-icon layui-icon-set-fill',
])->text('sort', '排序权重', 'Sort Order', false, '数值越大越靠前。', null, [ ]);
$icon->inputRightIcon('layui-icon-theme system-menu-icon-field-button', [
'data-open-menu-icon' => null,
'title' => BuilderLang::text('选择菜单图标'),
]);
});
$sortField = FormModules::gridColumn($grid, 3, 'system-menu-display-col system-menu-sort-col');
$sortField->fields(function ($fields) {
$fields->text('sort', '排序权重', 'Sort Order', false, '数值越大越靠前。', null, [
'type' => 'number', 'type' => 'number',
'min' => 0, 'min' => 0,
'placeholder' => '请输入排序权重', 'placeholder' => '请输入排序权重',
])->defaultValue(0)->radio('status', '使用状态', 'Status', '', true, [ ])->defaultValue(0);
});
$statusField = FormModules::gridColumn($grid, 3, 'system-menu-display-col system-menu-status-col');
$statusField->fields(function ($fields) {
$status = $fields->radio('status', '使用状态', 'Status', '', true, [
'required-error' => '请选择当前菜单的启用状态。', 'required-error' => '请选择当前菜单的启用状态。',
]); ]);
$status->body()->class('system-menu-status-options')->end()->options([ $status->body()->class('system-menu-status-options')->end()->options([
@ -251,25 +274,7 @@ SCRIPT);
0 => '已禁用', 0 => '已禁用',
])->defaultValue('1'); ])->defaultValue('1');
}); });
$section->div()->class('layui-form-mid color-desc system-menu-icon-preview')->html(sprintf(
$picker = $grid->div()->class('layui-col-xs12 layui-col-md6');
FormModules::pickerField($picker, [
'title' => '图标选择器',
'subtitle' => 'Icon Picker',
'value' => '点击打开图标选择器',
'class' => 'block system-menu-icon-picker',
'control_class' => 'relative',
'input_class' => 'layui-input pr40',
'attrs' => ['data-open-menu-icon' => null],
'input_attrs' => [
'data-open-menu-icon' => null,
'data-menu-icon-picker-text' => null,
'placeholder' => '点击选择图标并自动回填',
],
'icon_attrs' => ['data-open-menu-icon' => null],
'help' => '选择后会自动同步到“菜单图标”字段,并实时预览当前图标样式。',
]);
$picker->div()->class('layui-form-mid color-desc system-menu-icon-preview')->html(sprintf(
'<span class="inline-flex items-center"><i class="layui-icon layui-icon-set-fill font-s14 mr-8" data-menu-icon-preview></i><span data-menu-icon-preview-text>%s</span></span>', '<span class="inline-flex items-center"><i class="layui-icon layui-icon-set-fill font-s14 mr-8" data-menu-icon-preview></i><span data-menu-icon-preview-text>%s</span></span>',
self::escape(BuilderLang::text('未设置图标')) self::escape(BuilderLang::text('未设置图标'))
)); ));
@ -353,23 +358,24 @@ SCRIPT;
private static function renderFormStyle(): string private static function renderFormStyle(): string
{ {
return <<<'STYLE' return <<<'STYLE'
#MenuForm .system-menu-icon-picker .layui-input { #MenuForm .system-menu-section-note {
cursor: pointer; margin-top: -2px;
padding-right: 44px !important; margin-bottom: 12px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
} }
#MenuForm .system-menu-icon-picker .input-right-icon { #MenuForm .system-menu-preview-row {
top: 1px; margin-top: 2px;
right: 1px; }
width: 34px; #MenuForm .system-menu-preview-col .layui-input {
height: 32px; cursor: default;
line-height: 32px;
box-sizing: border-box;
border-left: 1px solid var(--ta-border-color, #dce8e5);
border-radius: 0 3px 3px 0;
} }
#MenuForm .system-menu-icon-preview { #MenuForm .system-menu-icon-preview {
display: block;
width: 100%;
min-height: 20px; min-height: 20px;
margin-top: 8px; margin-top: 0;
padding: 0 2px; padding: 0 2px;
line-height: 20px; line-height: 20px;
} }
@ -377,6 +383,29 @@ SCRIPT;
width: 18px; width: 18px;
text-align: center; text-align: center;
} }
#MenuForm [data-field-name="icon"] .layui-input {
padding-right: 44px !important;
}
#MenuForm [data-field-name="icon"] .system-menu-icon-field-button {
top: 1px;
right: 1px;
width: 34px;
height: 32px;
color: var(--ta-accent, #009688);
display: block;
cursor: pointer;
font-size: 16px;
line-height: 32px !important;
box-sizing: border-box;
text-align: center;
border-left: 1px solid var(--ta-border-color, #dce8e5);
border-radius: 0 6px 6px 0;
background: var(--ta-surface-soft, #f7fbfb) !important;
}
#MenuForm [data-field-name="icon"] .system-menu-icon-field-button:hover {
color: #fff;
background: var(--ta-accent, #009688) !important;
}
#MenuForm [data-field-name="status"] .system-menu-status-options { #MenuForm [data-field-name="status"] .system-menu-status-options {
height: auto !important; height: auto !important;
min-height: 34px !important; min-height: 34px !important;
@ -384,13 +413,16 @@ SCRIPT;
line-height: 32px !important; line-height: 32px !important;
align-items: center; align-items: center;
box-sizing: border-box; box-sizing: border-box;
overflow: hidden;
white-space: nowrap;
border-color: var(--ta-border-color, #dce8e5); border-color: var(--ta-border-color, #dce8e5);
background: var(--ta-surface-soft, #f7fbfb) !important; background: var(--ta-surface-soft, #f7fbfb) !important;
} }
#MenuForm [data-field-name="status"] .system-menu-status-options .layui-form-radio { #MenuForm [data-field-name="status"] .system-menu-status-options .layui-form-radio {
margin: 0 18px 0 0; margin: 0 12px 0 0;
padding-right: 0; padding-right: 0;
line-height: 32px; line-height: 32px;
white-space: nowrap;
} }
#MenuForm [data-field-name="status"] .system-menu-status-options .layui-form-radio > i { #MenuForm [data-field-name="status"] .system-menu-status-options .layui-form-radio > i {
margin-right: 5px; margin-right: 5px;
@ -404,6 +436,7 @@ STYLE;
return sprintf(<<<'SCRIPT' return sprintf(<<<'SCRIPT'
$.module.use(['jquery.autocompleter'], function () { $.module.use(['jquery.autocompleter'], function () {
var menuForm = document.getElementById('MenuForm') || {}; var menuForm = document.getElementById('MenuForm') || {};
var $form = $('#MenuForm');
var menuFormNodes = JSON.parse((menuForm.dataset || {}).menuNodes || '[]'); var menuFormNodes = JSON.parse((menuForm.dataset || {}).menuNodes || '[]');
var menuFormAuths = JSON.parse((menuForm.dataset || {}).menuAuths || '[]'); var menuFormAuths = JSON.parse((menuForm.dataset || {}).menuAuths || '[]');
var iconPickerUrl = String((menuForm.dataset || {}).menuIconPickerUrl || ''); var iconPickerUrl = String((menuForm.dataset || {}).menuIconPickerUrl || '');
@ -435,57 +468,60 @@ $.module.use(['jquery.autocompleter'], function () {
return parts.join('/'); return parts.join('/');
}; };
var syncIconPreview = function () { var syncIconPreview = function () {
var value = $.trim(String($('[name="icon"]').val() || '')); var value = $.trim(String($form.find('[name="icon"]').val() || ''));
var preview = $('[data-menu-icon-preview]').get(0); var preview = $form.find('[data-menu-icon-preview]').get(0);
if (preview) { if (preview) {
preview.className = value || 'layui-icon layui-icon-set-fill'; preview.className = value || 'layui-icon layui-icon-set-fill font-s14 mr-8';
} }
$('[data-menu-icon-preview-text]').text(value || menuI18n.iconNotSet); $form.find('[data-menu-icon-preview-text]').text(value || menuI18n.iconNotSet);
$('[data-menu-icon-picker-text]').val(value || '').attr('title', value || '');
}; };
var syncTargetPreview = function () { var syncTargetPreview = function () {
var url = $.trim(String($('[name="url"]').val() || '')); var url = $.trim(String($form.find('[name="url"]').val() || ''));
var params = $.trim(String($('[name="params"]').val() || '')).replace(/^[?&]+/, ''); var params = $.trim(String($form.find('[name="params"]').val() || '')).replace(/^[?&]+/, '');
var target = normalizeTarget(url); var target = normalizeTarget(url);
if (target !== '#' && params) { if (target !== '#' && params) {
target += (/^(https?:\/\/|\/\/)/i.test(target) && target.indexOf('?') >= 0 ? '&' : '?') + params; target += (/^(https?:\/\/|\/\/)/i.test(target) && target.indexOf('?') >= 0 ? '&' : '?') + params;
} }
$('[data-menu-url-preview]').val(target || '#'); $form.find('[data-menu-url-preview]').val(target || '#');
var node = normalizeNode($('[name="node"]').val()); var node = normalizeNode($form.find('[name="node"]').val());
if (!node && target !== '#' && !/^(https?:\/\/|\/\/|@|\[)/i.test(target)) { if (!node && target !== '#' && !/^(https?:\/\/|\/\/|@|\[)/i.test(target)) {
node = target.split('?')[0].split('/').slice(0, 3).join('/'); node = target.split('?')[0].split('/').slice(0, 3).join('/');
} }
$('[data-menu-node-preview]').val(node || menuI18n.nodeNotExplicitlySet); $form.find('[data-menu-node-preview]').val(node || menuI18n.nodeNotExplicitlySet);
}; };
$('body').off('click', '[data-open-menu-icon]').on('click', '[data-open-menu-icon]', function () { $('body').off('click', '[data-open-menu-icon]').on('click', '[data-open-menu-icon]', function () {
if (!iconPickerUrl) return false; if (!iconPickerUrl) return false;
$.form.modal(iconPickerUrl + (iconPickerUrl.indexOf('?') >= 0 ? '&' : '?') + 'field=' + encodeURIComponent('icon'), {}, menuI18n.chooseMenuIcon, undefined, undefined, undefined, '900px'); $.form.iframe(iconPickerUrl + (iconPickerUrl.indexOf('?') >= 0 ? '&' : '?') + 'field=' + encodeURIComponent('icon'), menuI18n.chooseMenuIcon, ['900px', '700px']);
return false; return false;
}); });
$('[name="icon"]').on('change input', syncIconPreview); $form.find('input[name=url]').autocompleter({
$('[name="url"], [name="params"], [name="node"]').on('change input', syncTargetPreview);
$('input[name=url]').autocompleter({
limit: 6, limit: 6,
customClass: ['ta-form-autocompleter'],
highlightMatches: true, highlightMatches: true,
template: '{{ label }} <span> {{ title }} </span>', template: '{{ label }} <span> {{ title }} </span>',
callback: function (node) { callback: function (node) {
if (!$('input[name=node]').val()) $('input[name=node]').val(node).trigger('change'); if (!$form.find('input[name=node]').val()) $form.find('input[name=node]').val(node).trigger('change');
syncTargetPreview();
}, },
source: (function (subjects, data) { source: (function (subjects, data) {
for (var i in subjects) data.push({value: subjects[i].node, label: subjects[i].node, title: subjects[i].title}); for (var i in subjects) data.push({value: subjects[i].node, label: subjects[i].node, title: subjects[i].title});
return data; return data;
})(menuFormNodes, []) })(menuFormNodes, [])
}); });
$('input[name=node]').autocompleter({ $form.find('input[name=node]').autocompleter({
limit: 5, limit: 5,
customClass: ['ta-form-autocompleter'],
highlightMatches: true, highlightMatches: true,
template: '{{ label }} <span> {{ title }} </span>', template: '{{ label }} <span> {{ title }} </span>',
callback: syncTargetPreview,
source: (function (subjects, data) { source: (function (subjects, data) {
for (var i in subjects) data.push({value: subjects[i].node, label: subjects[i].node, title: subjects[i].title}); for (var i in subjects) data.push({value: subjects[i].node, label: subjects[i].node, title: subjects[i].title});
return data; return data;
})(menuFormAuths, []) })(menuFormAuths, [])
}); });
$form.find('[name="icon"]').on('change input', syncIconPreview);
$form.find('[name="url"], [name="params"], [name="node"]').on('change input', syncTargetPreview);
syncIconPreview(); syncIconPreview();
syncTargetPreview(); syncTargetPreview();
}); });

View File

@ -159,10 +159,38 @@ class MenuControllerTest extends SqliteIntegrationTestCase
$this->assertStringContainsString('form-builder-schema', $html); $this->assertStringContainsString('form-builder-schema', $html);
$this->assertStringContainsString('name="title"', $html); $this->assertStringContainsString('name="title"', $html);
$this->assertStringContainsString('ta-form-nowrap', $html);
$this->assertStringContainsString('data-form-builder-standard-style', $html);
$this->assertStringContainsString('system-menu-form-row', $html);
$this->assertStringContainsString('layui-col-space8', $html);
$this->assertStringContainsString('ta-form-grid-compact', $html);
$this->assertStringContainsString('ta-form-grid-2', $html);
$this->assertStringContainsString('ta-form-grid-3', $html);
$this->assertStringContainsString('layui-col-xs6 ta-form-grid-col', $html);
$this->assertStringContainsString('layui-col-xs4 ta-form-grid-col', $html);
$this->assertStringContainsString('width:50%!important', $html);
$this->assertStringContainsString('width:33.333333%!important', $html);
$this->assertStringContainsString('margin-bottom:6px', $html);
$this->assertStringContainsString('system-menu-basic-col', $html);
$this->assertStringContainsString('system-menu-display-col', $html);
$this->assertStringContainsString('data-menu-nodes=', $html); $this->assertStringContainsString('data-menu-nodes=', $html);
$this->assertStringContainsString('data-menu-auths=', $html); $this->assertStringContainsString('data-menu-auths=', $html);
$this->assertStringContainsString('data-open-menu-icon', $html); $this->assertStringContainsString('data-open-menu-icon', $html);
$this->assertStringContainsString('标准化链接预览', $html); $this->assertStringContainsString('ta-form-autocompleter', $html);
$this->assertStringContainsString('top:58px', $html);
$this->assertStringContainsString('层级最多三级', $html);
$this->assertStringContainsString('链接预览', $html);
$this->assertStringContainsString('权限预览', $html);
$this->assertStringContainsString('data-menu-url-preview', $html);
$this->assertStringContainsString('data-menu-node-preview', $html);
$this->assertStringContainsString('data-menu-icon-preview', $html);
$this->assertStringContainsString('选择上级,叶子菜单不可挂载。', $html);
$this->assertStringContainsString('GET 参数,如 tab=user。', $html);
$this->assertStringNotContainsString('当前仅支持三级菜单结构', $html);
$this->assertStringNotContainsString('保存时会自动标准化链接格式', $html);
$this->assertStringNotContainsString('标准化链接预览', $html);
$this->assertStringNotContainsString('鉴权节点预览', $html);
$this->assertStringNotContainsString('图标选择器', $html);
$this->assertStringContainsString('name="target"', $html); $this->assertStringContainsString('name="target"', $html);
} }
@ -172,10 +200,10 @@ class MenuControllerTest extends SqliteIntegrationTestCase
$html = $this->callActionHtml('add', ['pid' => 0]); $html = $this->callActionHtml('add', ['pid' => 0]);
$this->assertStringContainsString('Icon Picker', $html);
$this->assertStringContainsString('Icon not set', $html);
$this->assertStringContainsString('Choose Menu Icon', $html); $this->assertStringContainsString('Choose Menu Icon', $html);
$this->assertStringNotContainsString('未设置图标', $html); $this->assertStringContainsString('Icon not set', $html);
$this->assertStringNotContainsString('Icon Picker', $html);
$this->assertStringNotContainsString('选择菜单图标', $html);
} }
public function testAddAndEditPersistMenuFields(): void public function testAddAndEditPersistMenuFields(): void