feat(nav): complete remaining zibll parity (search, i18n, senior menus, CSS)

- Remove obsolete dual-path nav CSS; add utilities + language dropdown styles
- Wire header/mobile translate switcher from i18n config
- Layout-2 inline search hot keywords panel; nav_folding on resize
- NavMenuOptions JSON editors for graphic_card / multi_column_links
- Section-navs zibll posts-nav-lists; page-navs enqueue; mobile article TOC box
- SPA/PHP user-card publish type; rebuild admin assets
This commit is contained in:
2026-07-22 11:40:17 +08:00
parent 259dbdbe04
commit fc50e57bc9
22 changed files with 580 additions and 1994 deletions
+152 -13
View File
@@ -1,6 +1,6 @@
<?php
/**
* Native WP nav-menu item options (aksrt_menu_options).
* Native WP nav-menu item options (aksrt_menu_options) with senior submenu JSON.
*
* @package AKSRT
*/
@@ -29,6 +29,12 @@ class NavMenuOptions {
$badge_class = (string) ($opts['badge_class'] ?? '');
$submenu_s = !empty($opts['submenu_s']);
$submenu_type = (string) ($opts['submenu_type'] ?? '');
$graphic_json = !empty($opts['graphic_card_opts'])
? wp_json_encode($opts['graphic_card_opts'], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)
: self::sample_graphic_json();
$multi_json = !empty($opts['multi_column_links_opts'])
? wp_json_encode($opts['multi_column_links_opts'], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)
: self::sample_multi_json();
?>
<p class="field-aksrt-icon description description-wide">
<label for="edit-menu-item-aksrt-icon-<?php echo esc_attr((string) $item_id); ?>">
@@ -57,7 +63,7 @@ class NavMenuOptions {
<p class="field-aksrt-submenu description description-wide">
<label>
<input type="checkbox" name="aksrt_menu_options[<?php echo esc_attr((string) $item_id); ?>][submenu_s]" value="1" <?php checked($submenu_s); ?>>
<?php esc_html_e('启用高级子菜单(一级菜单)', 'aksrt'); ?>
<?php esc_html_e('启用高级子菜单(仅一级菜单)', 'aksrt'); ?>
</label>
</p>
<p class="field-aksrt-submenu-type description description-wide">
@@ -71,8 +77,19 @@ class NavMenuOptions {
</select>
</label>
</p>
<p class="description description-wide">
<?php esc_html_e('高级子菜单内容(graphic_card_opts / multi_column_links_opts)可在后续可视化编辑;也可通过 meta 写入 JSON。', 'aksrt'); ?>
<p class="field-aksrt-graphic description description-wide">
<label for="edit-menu-item-aksrt-graphic-<?php echo esc_attr((string) $item_id); ?>">
<?php esc_html_e('图文卡片 JSON(type=graphic_card)', 'aksrt'); ?><br>
<textarea rows="8" class="widefat code" id="edit-menu-item-aksrt-graphic-<?php echo esc_attr((string) $item_id); ?>"
name="aksrt_menu_options[<?php echo esc_attr((string) $item_id); ?>][graphic_card_opts_json]"><?php echo esc_textarea((string) $graphic_json); ?></textarea>
</label>
</p>
<p class="field-aksrt-multi description description-wide">
<label for="edit-menu-item-aksrt-multi-<?php echo esc_attr((string) $item_id); ?>">
<?php esc_html_e('多列链接 JSON(type=multi_column_links)', 'aksrt'); ?><br>
<textarea rows="8" class="widefat code" id="edit-menu-item-aksrt-multi-<?php echo esc_attr((string) $item_id); ?>"
name="aksrt_menu_options[<?php echo esc_attr((string) $item_id); ?>][multi_column_links_opts_json]"><?php echo esc_textarea((string) $multi_json); ?></textarea>
</label>
</p>
<?php
}
@@ -83,15 +100,12 @@ class NavMenuOptions {
*/
public static function save($menu_id, $menu_item_db_id): void {
$menu_item_db_id = (int) $menu_item_db_id;
// phpcs:ignore WordPress.Security.NonceVerification.Missing -- WP core nav-menu form nonce.
// phpcs:ignore WordPress.Security.NonceVerification.Missing
if (empty($_POST['aksrt_menu_options'][ $menu_item_db_id ]) || !is_array($_POST['aksrt_menu_options'][ $menu_item_db_id ])) {
// If fields absent, leave existing meta (WP may not resubmit all fields in bulk edits).
return;
}
// phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
$raw = wp_unslash($_POST['aksrt_menu_options'][ $menu_item_db_id ]);
$existing = get_post_meta($menu_item_db_id, 'aksrt_menu_options', true);
$existing = is_array($existing) ? $existing : [];
$clean = [
'icon' => sanitize_text_field((string) ($raw['icon'] ?? '')),
@@ -100,13 +114,138 @@ class NavMenuOptions {
'submenu_s' => !empty($raw['submenu_s']),
'submenu_type' => sanitize_key((string) ($raw['submenu_type'] ?? '')),
];
// Preserve complex opts not edited by this form.
if (!empty($existing['graphic_card_opts'])) {
$clean['graphic_card_opts'] = $existing['graphic_card_opts'];
$graphic = self::decode_json_field((string) ($raw['graphic_card_opts_json'] ?? ''));
if (is_array($graphic)) {
$clean['graphic_card_opts'] = self::sanitize_graphic_opts($graphic);
}
if (!empty($existing['multi_column_links_opts'])) {
$clean['multi_column_links_opts'] = $existing['multi_column_links_opts'];
$multi = self::decode_json_field((string) ($raw['multi_column_links_opts_json'] ?? ''));
if (is_array($multi)) {
$clean['multi_column_links_opts'] = self::sanitize_multi_opts($multi);
}
update_post_meta($menu_item_db_id, 'aksrt_menu_options', $clean);
}
/**
* @return array<string,mixed>|null
*/
private static function decode_json_field(string $json): ?array {
$json = trim($json);
if ($json === '') {
return null;
}
$data = json_decode($json, true);
return is_array($data) ? $data : null;
}
/**
* @param array<string,mixed> $opts
* @return array<string,mixed>
*/
private static function sanitize_graphic_opts(array $opts): array {
$items_in = is_array($opts['items'] ?? null) ? $opts['items'] : [];
$items = [];
foreach ($items_in as $item) {
if (!is_array($item)) {
continue;
}
$items[] = [
'img' => esc_url_raw((string) ($item['img'] ?? '')),
'title' => sanitize_text_field((string) ($item['title'] ?? '')),
'badge' => sanitize_text_field((string) ($item['badge'] ?? '')),
'badge_class' => sanitize_html_class((string) ($item['badge_class'] ?? '')),
'link' => [
'url' => esc_url_raw((string) ($item['link']['url'] ?? '')),
'text' => sanitize_text_field((string) ($item['link']['text'] ?? '')),
'target' => sanitize_text_field((string) ($item['link']['target'] ?? '')),
],
];
}
return [
'arrangement' => sanitize_key((string) ($opts['arrangement'] ?? '')),
'align' => sanitize_key((string) ($opts['align'] ?? '')),
'size' => sanitize_key((string) ($opts['size'] ?? '')),
'img_scale' => max(0, min(200, (int) ($opts['img_scale'] ?? 100))),
'items' => $items,
];
}
/**
* @param array<string,mixed> $opts
* @return array<string,mixed>
*/
private static function sanitize_multi_opts(array $opts): array {
$columns_in = is_array($opts['columns'] ?? null) ? $opts['columns'] : [];
$columns = [];
foreach ($columns_in as $col) {
if (!is_array($col)) {
continue;
}
$items = [];
if (!empty($col['items']) && is_array($col['items'])) {
foreach ($col['items'] as $item) {
if (!is_array($item)) {
continue;
}
$items[] = [
'title' => sanitize_text_field((string) ($item['title'] ?? '')),
'icon' => sanitize_text_field((string) ($item['icon'] ?? '')),
'img_icon' => esc_url_raw((string) ($item['img_icon'] ?? '')),
'link' => [
'url' => esc_url_raw((string) ($item['link']['url'] ?? '')),
'text' => sanitize_text_field((string) ($item['link']['text'] ?? '')),
'target' => sanitize_text_field((string) ($item['link']['target'] ?? '')),
],
];
}
}
$columns[] = [
'title' => sanitize_text_field((string) ($col['title'] ?? '')),
'icon' => sanitize_text_field((string) ($col['icon'] ?? '')),
'img_icon' => esc_url_raw((string) ($col['img_icon'] ?? '')),
'link' => [
'url' => esc_url_raw((string) ($col['link']['url'] ?? '')),
'text' => sanitize_text_field((string) ($col['link']['text'] ?? '')),
'target' => sanitize_text_field((string) ($col['link']['target'] ?? '')),
],
'items' => $items,
];
}
return ['columns' => $columns];
}
private static function sample_graphic_json(): string {
$sample = [
'arrangement' => '',
'size' => 'md',
'img_scale' => 100,
'items' => [
[
'img' => '',
'title' => '卡片标题',
'badge' => '',
'badge_class' => 'jb-red',
'link' => ['url' => home_url('/'), 'text' => '', 'target' => ''],
],
],
];
return (string) wp_json_encode($sample, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
}
private static function sample_multi_json(): string {
$sample = [
'columns' => [
[
'title' => '栏目一',
'icon' => 'fas fa-folder',
'link' => ['url' => '', 'text' => '', 'target' => ''],
'items' => [
['title' => '链接 A', 'icon' => '', 'link' => ['url' => home_url('/'), 'text' => '', 'target' => '']],
],
],
],
];
return (string) wp_json_encode($sample, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
}
}
@@ -64,6 +64,11 @@ class SchemaPageDisplay {
['label' => '帖子', 'value' => 'topic'],
['label' => '商品', 'value' => 'product'],
]],
['key' => 'header_user_newposts_btn', 'label' => '用户卡发布类型', 'type' => 'select', 'default' => 'post', 'options' => [
['label' => '文章', 'value' => 'post'],
['label' => '帖子', 'value' => 'topic'],
['label' => '商品', 'value' => 'product'],
]],
['key' => 'header_user_newposts_text', 'label' => '用户卡发布文案', 'type' => 'input', 'default' => '发布内容'],
]],
['key' => 'color', 'title' => '颜色设置', 'fields' => [