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
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
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') },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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; }
|
||||
|
||||
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 18 KiB |
@@ -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, '"') +
|
||||
'" 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);
|
||||
@@ -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();
|
||||
}
|
||||
})();
|
||||