feat(nav): finish remaining parity gaps (image_select, AJAX search, senior UI)

- Layout image_select previews (zibll PNGs under assets/images/header-layout)
- Localize aksrtAdminData.themeUri for admin SPA asset URLs
- Layout-2 inline search AJAX suggestions via aksrt_search_suggestions
- Appearance→Menus visual senior submenu editor (cards/columns)
- Acceptance checklist + automated source contract report
This commit is contained in:
2026-07-22 12:04:44 +08:00
parent e3848ddf8c
commit 2a11a889c3
20 changed files with 522 additions and 19 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

@@ -3,6 +3,14 @@
* Each key corresponds to a sub-page under the "页面&显示" section.
*/
/** Theme asset base for layout previews (admin SPA). */
function themeAsset(path) {
const base = (typeof window !== 'undefined' && window.aksrtAdminData && window.aksrtAdminData.themeUri)
? String(window.aksrtAdminData.themeUri).replace(/\/$/, '')
: ''
return base ? `${base}/${path.replace(/^\//, '')}` : path
}
/** Homepage sort options — keep in sync with AKSRT_Settings_Home_Cms::homepage_allowed_sorts() */
const sortOptions = [
{ label: '最近更新', value: 'modified' },
@@ -79,23 +87,23 @@ export const pageDisplaySchemas = {
{
key: 'header_layout',
label: '电脑端布局',
type: 'radio',
default: '2',
type: 'image_select',
default: '1',
options: [
{ label: '布局 1(经典)', value: '1' },
{ label: '布局 2(内联搜索)', value: '2' },
{ label: '布局 3(紧凑)', value: '3' },
{ label: '布局 1(经典)', value: '1', image: themeAsset('assets/images/header-layout/header_layout_1.png') },
{ label: '布局 2(内联搜索)', value: '2', image: themeAsset('assets/images/header-layout/header_layout_2.png') },
{ label: '布局 3(紧凑)', value: '3', image: themeAsset('assets/images/header-layout/header_layout_3.png') },
],
help: '不同布局切换导航 DOM 结构',
help: '不同布局切换导航 DOM 结构(与 zibll image_select 一致)',
},
{
key: 'mobile_header_layout',
label: '移动端布局',
type: 'radio',
type: 'image_select',
default: 'center',
options: [
{ label: '居中(含搜索按钮)', value: 'center' },
{ label: '靠左', value: 'left' },
{ label: '居中(含搜索按钮)', value: 'center', image: themeAsset('assets/images/header-layout/mobile_header_layout_center.png') },
{ label: '靠左', value: 'left', image: themeAsset('assets/images/header-layout/mobile_header_layout_left.png') },
],
},
{
+32
View File
@@ -130,3 +130,35 @@ body.mobile-navbar-show { overflow: hidden; }
border-radius: 0;
z-index: 998;
}
.navbar-inline-search-panel {
position: absolute;
left: 0;
top: calc(100% + 8px);
min-width: 280px;
max-width: 360px;
padding: 12px;
background: var(--main-bg-color, #fff);
border-radius: var(--main-radius, 8px);
box-shadow: 0 8px 24px rgba(0,0,0,.12);
z-index: 1002;
display: none;
}
.navbar-form .form-group:hover .navbar-inline-search-panel,
.navbar-form .form-group:focus-within .navbar-inline-search-panel,
.navbar-inline-search-panel.is-open {
display: block;
visibility: visible;
}
.navbar-inline-search-suggest__item {
display: flex;
flex-direction: column;
padding: 6px 4px;
color: inherit;
text-decoration: none;
}
.navbar-inline-search-suggest__item:hover { color: var(--focus-color); }
.navbar-inline-search-hots .but { margin: 0 4px 4px 0; }
.aksrt-sme-field { display: block; margin: 4px 0; }
.aksrt-sme-field span { display: block; font-size: 12px; color: #646970; }
.aksrt-sme-card, .aksrt-sme-col { border: 1px solid #dcdcde; padding: 8px; margin: 8px 0; border-radius: 4px; }
Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

+243
View File
@@ -0,0 +1,243 @@
/**
* Nav menu senior submenu visual editors (graphic cards / multi-column).
* Enhances WP Appearance → Menus fields for aksrt_menu_options JSON.
*
* @package AKSRT
*/
(function ($) {
'use strict';
function parseJSON(raw, fallback) {
try {
var data = JSON.parse(raw || '');
return data && typeof data === 'object' ? data : fallback;
} catch (e) {
return fallback;
}
}
function graphicTemplate() {
return {
arrangement: '',
size: 'md',
img_scale: 100,
items: [{ img: '', title: '', badge: '', badge_class: 'jb-red', link: { url: '', text: '', target: '' } }],
};
}
function multiTemplate() {
return {
columns: [
{
title: '',
icon: '',
img_icon: '',
link: { url: '', text: '', target: '' },
items: [{ title: '', icon: '', img_icon: '', link: { url: '', text: '', target: '' } }],
},
],
};
}
function field(name, label, value, type) {
type = type || 'text';
return (
'<label class="aksrt-sme-field"><span>' +
label +
'</span><input type="' +
type +
'" data-name="' +
name +
'" value="' +
String(value || '').replace(/"/g, '&quot;') +
'" class="widefat"></label>'
);
}
function renderGraphicUI($wrap, data) {
var html = '<div class="aksrt-sme-toolbar">';
html += '<button type="button" class="button aksrt-sme-add-card">添加卡片</button>';
html += '</div><div class="aksrt-sme-cards">';
(data.items || []).forEach(function (item, i) {
html += '<div class="aksrt-sme-card" data-index="' + i + '">';
html += '<div class="aksrt-sme-card-head"><strong>卡片 #' + (i + 1) + '</strong>';
html += ' <button type="button" class="button-link-delete aksrt-sme-del-card">删除</button></div>';
html += field('img', '图片 URL', item.img || '');
html += field('title', '标题', item.title || '');
html += field('badge', '徽章', item.badge || '');
html += field('badge_class', '徽章 class', item.badge_class || '');
html += field('link.url', '链接', (item.link && item.link.url) || '');
html += '</div>';
});
html += '</div>';
html += field('size', '尺寸 sm/md/lg/xs', data.size || 'md');
html += field('img_scale', '图片比例%', data.img_scale || 100, 'number');
$wrap.html(html);
}
function renderMultiUI($wrap, data) {
var html = '<div class="aksrt-sme-toolbar">';
html += '<button type="button" class="button aksrt-sme-add-col">添加栏目</button>';
html += '</div><div class="aksrt-sme-cols">';
(data.columns || []).forEach(function (col, i) {
html += '<div class="aksrt-sme-col" data-index="' + i + '">';
html += '<div class="aksrt-sme-card-head"><strong>栏目 #' + (i + 1) + '</strong>';
html += ' <button type="button" class="button-link-delete aksrt-sme-del-col">删除栏目</button></div>';
html += field('title', '栏目标题', col.title || '');
html += field('icon', '图标 class', col.icon || '');
html += field('link.url', '栏目链接', (col.link && col.link.url) || '');
html += '<div class="aksrt-sme-items">';
(col.items || []).forEach(function (item, j) {
html += '<div class="aksrt-sme-item" data-j="' + j + '">';
html += field('items.' + j + '.title', '链接标题', item.title || '');
html += field('items.' + j + '.link.url', '链接 URL', (item.link && item.link.url) || '');
html += field('items.' + j + '.icon', '图标', item.icon || '');
html += '</div>';
});
html += '<button type="button" class="button aksrt-sme-add-item">添加链接</button>';
html += '</div></div>';
});
html += '</div>';
$wrap.html(html);
}
function readGraphic($wrap) {
var data = graphicTemplate();
data.size = $wrap.find('[data-name="size"]').val() || 'md';
data.img_scale = parseInt($wrap.find('[data-name="img_scale"]').val(), 10) || 100;
data.items = [];
$wrap.find('.aksrt-sme-card').each(function () {
var $c = $(this);
data.items.push({
img: $c.find('[data-name="img"]').val() || '',
title: $c.find('[data-name="title"]').val() || '',
badge: $c.find('[data-name="badge"]').val() || '',
badge_class: $c.find('[data-name="badge_class"]').val() || '',
link: { url: $c.find('[data-name="link.url"]').val() || '', text: '', target: '' },
});
});
return data;
}
function readMulti($wrap) {
var data = { columns: [] };
$wrap.find('.aksrt-sme-col').each(function () {
var $col = $(this);
var col = {
title: $col.find('[data-name="title"]').first().val() || '',
icon: $col.find('[data-name="icon"]').first().val() || '',
img_icon: '',
link: { url: $col.find('[data-name="link.url"]').first().val() || '', text: '', target: '' },
items: [],
};
$col.find('.aksrt-sme-item').each(function () {
var $it = $(this);
col.items.push({
title: $it.find('input[data-name$=".title"]').val() || '',
icon: $it.find('input[data-name$=".icon"]').val() || '',
img_icon: '',
link: { url: $it.find('input[data-name$=".link.url"]').val() || '', text: '', target: '' },
});
});
data.columns.push(col);
});
return data;
}
function enhanceItem($item) {
if ($item.data('aksrtSme')) return;
$item.data('aksrtSme', 1);
var $graphicTa = $item.find('textarea[name*="[graphic_card_opts_json]"]');
var $multiTa = $item.find('textarea[name*="[multi_column_links_opts_json]"]');
var $type = $item.find('select[name*="[submenu_type]"]');
if (!$graphicTa.length && !$multiTa.length) return;
var $ui = $('<div class="aksrt-sme-ui" style="margin:8px 0;padding:10px;border:1px solid #dcdcde;background:#fff;border-radius:4px;"></div>');
$graphicTa.closest('p').before($ui);
function sync() {
var type = $type.val();
if (type === 'graphic_card') {
$graphicTa.closest('p').hide();
$multiTa.closest('p').hide();
var g = parseJSON($graphicTa.val(), graphicTemplate());
renderGraphicUI($ui, g);
$ui.show();
} else if (type === 'multi_column_links') {
$graphicTa.closest('p').hide();
$multiTa.closest('p').hide();
var m = parseJSON($multiTa.val(), multiTemplate());
renderMultiUI($ui, m);
$ui.show();
} else {
$ui.hide().empty();
$graphicTa.closest('p').show();
$multiTa.closest('p').show();
}
}
$type.on('change', sync);
$ui.on('click', '.aksrt-sme-add-card', function (e) {
e.preventDefault();
var g = readGraphic($ui);
g.items.push({ img: '', title: '', badge: '', badge_class: 'jb-red', link: { url: '', text: '', target: '' } });
renderGraphicUI($ui, g);
});
$ui.on('click', '.aksrt-sme-del-card', function (e) {
e.preventDefault();
$(this).closest('.aksrt-sme-card').remove();
});
$ui.on('click', '.aksrt-sme-add-col', function (e) {
e.preventDefault();
var m = readMulti($ui);
m.columns.push({
title: '',
icon: '',
img_icon: '',
link: { url: '', text: '', target: '' },
items: [{ title: '', icon: '', img_icon: '', link: { url: '', text: '', target: '' } }],
});
renderMultiUI($ui, m);
});
$ui.on('click', '.aksrt-sme-del-col', function (e) {
e.preventDefault();
$(this).closest('.aksrt-sme-col').remove();
});
$ui.on('click', '.aksrt-sme-add-item', function (e) {
e.preventDefault();
var $col = $(this).closest('.aksrt-sme-col');
var m = readMulti($ui);
var idx = $col.index();
if (m.columns[idx]) {
m.columns[idx].items.push({ title: '', icon: '', img_icon: '', link: { url: '', text: '', target: '' } });
}
renderMultiUI($ui, m);
});
// Before save, write UI → textarea
$('#update-nav-menu').on('submit.aksrtSme', function () {
var type = $type.val();
if (type === 'graphic_card') {
$graphicTa.val(JSON.stringify(readGraphic($ui), null, 2));
} else if (type === 'multi_column_links') {
$multiTa.val(JSON.stringify(readMulti($ui), null, 2));
}
});
// Hide raw JSON by default; toggle available via double-click title
$graphicTa.closest('p').find('label').attr('title', '双击显示原始 JSON');
sync();
}
function boot() {
$('#menu-to-edit .menu-item').each(function () {
enhanceItem($(this));
});
}
$(boot);
$(document).on('menu-item-added', function () {
setTimeout(boot, 50);
});
})(jQuery);
+140
View File
@@ -0,0 +1,140 @@
/**
* Enhance layout-2 inline search with AJAX suggestions (aksrt_search_suggestions).
* Extends existing panel search without dual systems.
*/
(function () {
"use strict";
function initInlineSearchSuggest() {
var form = document.querySelector('form[data-aksrt-search-form="inline"]');
if (!form || !window.aksrtData || !aksrtData.ajaxUrl) return;
var input = form.querySelector(".search-input");
var group = form.querySelector(".form-group.relative, .form-group");
if (!input || !group) return;
var panel = group.querySelector(".navbar-inline-search-panel");
if (!panel) {
panel = document.createElement("div");
panel.className = "dropdown-menu hover-show-con navbar-inline-search-panel";
group.appendChild(panel);
}
var timer = null;
var searchConfig = (window.aksrtData && aksrtData.search) || {};
var suggestionsEnabled = searchConfig.suggestionsEnabled !== false;
function typeValue() {
var t = form.querySelector('input[name="aksrt_search_type"]');
return (t && t.value) || searchConfig.defaultType || "post";
}
function renderItems(items) {
var html = "";
if (items && items.length) {
html += '<div class="muted-2-color em09 mb6">' + (searchConfig.suggestTitle || "搜索建议") + "</div>";
html += '<div class="navbar-inline-search-suggest">';
items.forEach(function (it) {
html +=
'<a class="navbar-inline-search-suggest__item" href="' +
(it.url || "#") +
'"><span class="title">' +
(it.title || "") +
'</span>' +
(it.meta ? '<span class="meta muted-2-color em09">' + it.meta + "</span>" : "") +
"</a>";
});
html += "</div>";
}
// Keep hot keywords block if present as static sibling content was replaced —
// re-append hots from data attribute if we stored them.
var hots = form.getAttribute("data-hots");
if (hots) {
try {
var arr = JSON.parse(hots);
if (arr && arr.length) {
html += '<div class="muted-2-color em09 mb6 mt10">热门搜索</div><div class="navbar-inline-search-hots">';
arr.forEach(function (kw) {
html +=
'<a class="but jb-sm" href="' +
(window.location.origin || "") +
"/?s=" +
encodeURIComponent(kw) +
'">' +
kw +
"</a> ";
});
html += "</div>";
}
} catch (e) {}
}
panel.innerHTML = html || panel.innerHTML;
panel.classList.add("is-open");
panel.style.display = "block";
panel.style.visibility = "visible";
}
// Persist hot keywords for re-render after suggestions
var hotLinks = panel.querySelectorAll(".navbar-inline-search-hots a");
if (hotLinks.length) {
var kws = [];
hotLinks.forEach(function (a) {
kws.push((a.textContent || "").trim());
});
form.setAttribute("data-hots", JSON.stringify(kws));
}
function request(q) {
if (!suggestionsEnabled || q.length < 2) {
// restore hots-only
if (form.getAttribute("data-hots")) {
renderItems([]);
}
return;
}
var xhr = new XMLHttpRequest();
xhr.open("POST", aksrtData.ajaxUrl, true);
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8");
xhr.onload = function () {
if (xhr.status < 200 || xhr.status >= 300) return;
try {
var json = JSON.parse(xhr.responseText);
if (json.success && json.data && json.data.items) {
renderItems(json.data.items);
}
} catch (e) {}
};
xhr.send(
"action=aksrt_search_suggestions&nonce=" +
encodeURIComponent(aksrtData.nonce) +
"&q=" +
encodeURIComponent(q) +
"&type=" +
encodeURIComponent(typeValue())
);
}
input.addEventListener("input", function () {
var q = (input.value || "").trim();
if (timer) clearTimeout(timer);
timer = setTimeout(function () {
request(q);
}, 180);
});
input.addEventListener("focus", function () {
panel.style.display = "block";
panel.style.visibility = "visible";
});
}
if (window.AksrtFrontend && typeof window.AksrtFrontend.register === "function") {
window.AksrtFrontend.register("aksrt-inline-search-suggest", function () {
initInlineSearchSuggest();
});
} else if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", initInlineSearchSuggest);
} else {
initInlineSearchSuggest();
}
})();