Создание динамического меню с помощью вложенных наборов

Я пытаюсь создать динамическое меню в моей PHP CMS; страницы / категории организованы с использованием модели вложенных множеств.

Полное дерево:

 корень
 
  В
   B1
    B1.1
    B1.2
   Би 2
    B2.1
    B2.1
  С
   С1
   С2
   C3
  D

Я хочу преобразовать этот результирующий набор в список unordererd, который отображает только часть дерева. Например: если я нажимаю на B, я хочу показать только следующую часть списка:


 В
  B1
  Би 2
 С
 D

Затем, если я нажму на B1, я хочу показать этот список:


 В
  B1
   B1.1
   B1.2
  Би 2
 С
 D

и т.п.

Я использую следующий SQL-запрос для получения всех узлов из базы данных (mysql):

  SELECT node.id, node.lft, node.rgt, node.name, 
 GROUP_CONCAT (parent.name ORDER BY parent.lft SEPARATOR "/") AS путь, 
 (COUNT (parent.lft) - 1) AS depth 
 FROM страницы AS node, страницы AS parent 
 WHERE node.lft BETWEEN parent.lft И parent.rgt 
 AND (parent.hidden = "no" И node.hidden = "no") AND parent.lft> 1 
 GROUP BY node.id ORDER BY node.lft 

Мне удалось создать полный список без рекурсии (используя столбец глубины), но я не могу отфильтровать меню, как показано выше; Я думаю, мне нужно получить значение lft и rgt родителя для каждого узла и отфильтровать элементы с помощью PHP. Но как я могу получить эти значения в одном и том же запросе?

Есть ли другие предложения о том, как этого достичь?

Заранее спасибо!

Следующий запрос позволит вам открыть любой путь (или набор путей), воспользовавшись предложением SQL и функцией group_concat MySQL.

Ниже приведено определение таблицы и примеры данных, которые я использовал:

drop table nested_set; CREATE TABLE nested_set ( id INT, name VARCHAR(20) NOT NULL, lft INT NOT NULL, rgt INT NOT NULL ); INSERT INTO nested_set (id, name, lft, rgt) VALUES (1,'HEAD',1,28); INSERT INTO nested_set (id, name, lft, rgt) VALUES (2,'A',2,3); INSERT INTO nested_set (id, name, lft, rgt) VALUES (3,'B',4,17); INSERT INTO nested_set (id, name, lft, rgt) VALUES (4,'B1',5,10); INSERT INTO nested_set (id, name, lft, rgt) VALUES (5,'B1.1',6,7); INSERT INTO nested_set (id, name, lft, rgt) VALUES (6,'B1.2',8,9); INSERT INTO nested_set (id, name, lft, rgt) VALUES (7,'B2',11,16); INSERT INTO nested_set (id, name, lft, rgt) VALUES (8,'B2.1',12,13); INSERT INTO nested_set (id, name, lft, rgt) VALUES (9,'B2.2',14,15); INSERT INTO nested_set (id, name, lft, rgt) VALUES (10,'C',18,25); INSERT INTO nested_set (id, name, lft, rgt) VALUES (11,'C1',19,20); INSERT INTO nested_set (id, name, lft, rgt) VALUES (12,'C2',21,22); INSERT INTO nested_set (id, name, lft, rgt) VALUES (13,'C3',23,24); INSERT INTO nested_set (id, name, lft, rgt) VALUES (14,'D',26,27); 

Следующий запрос дает вам полное дерево (кроме HEAD):

 SELECT node.id , node.lft , node.rgt , node.name , GROUP_CONCAT(parent.name ORDER BY parent.lft SEPARATOR "/" ) AS path , (COUNT(parent.lft) - 1) AS depth FROM nested_set AS node inner join nested_set AS parent on node.lft BETWEEN parent.lft AND parent.rgt where parent.lft > 1 GROUP BY node.id 

При выводе следующих данных:

 +------+-----+-----+------+-----------+-------+ | id | lft | rgt | name | path | depth | +------+-----+-----+------+-----------+-------+ | 2 | 2 | 3 | A | A | 0 | | 3 | 4 | 17 | B | B | 0 | | 4 | 5 | 10 | B1 | B/B1 | 1 | | 5 | 6 | 7 | B1.1 | B/B1/B1.1 | 2 | | 6 | 8 | 9 | B1.2 | B/B1/B1.2 | 2 | | 7 | 11 | 16 | B2 | B/B2 | 1 | | 8 | 12 | 13 | B2.1 | B/B2/B2.1 | 2 | | 9 | 14 | 15 | B2.2 | B/B2/B2.2 | 2 | | 10 | 18 | 25 | C | C | 0 | | 11 | 19 | 20 | C1 | C/C1 | 1 | | 12 | 21 | 22 | C2 | C/C2 | 1 | | 13 | 23 | 24 | C3 | C/C3 | 1 | | 14 | 26 | 27 | D | D | 0 | +------+-----+-----+------+-----------+-------+ 

Следующие дополнения к вышеуказанному запросу предоставят вам контроль, необходимый для открытия различных разделов:

 having depth = 0 or ('<PATH_TO_OPEN>' = left(path, length('<PATH_TO_OPEN>')) and depth = length('<PATH_TO_OPEN>') - length(replace('<PATH_TO_OPEN>', '/', '')) + 1) 

Предложение having применяет фильтры к результатам группы по запросу. Часть «depth = 0» должна гарантировать, что у нас всегда есть узлы базового меню (A, B, C и D). Следующая часть – это часть, которая контролирует, какие узлы открыты. Он сравнивает путь узлов с заданным путём, который вы хотите открыть (''), чтобы увидеть, совпадает ли он, и он также гарантирует, что он только откроет уровень в пути. Вся или секция с логикой '' может быть дублирована и добавлена ​​по мере необходимости для открытия нескольких путей по мере необходимости. Убедитесь, что '' не заканчивается конечной косой чертой (/).

Ниже приведены некоторые примеры вывода, показывающие, как вы будете строить запросы для получения желаемых результатов:

 =========Open B========== SELECT node.id , node.lft , node.rgt , node.name , GROUP_CONCAT(parent.name ORDER BY parent.lft SEPARATOR "/" ) AS path , (COUNT(parent.lft) - 1) AS depth FROM nested_set AS node inner join nested_set AS parent on node.lft BETWEEN parent.lft AND parent.rgt where parent.lft > 1 GROUP BY node.id having depth = 0 or ('B' = left(path, length('B')) and depth = length('B') - length(replace('B', '/', '')) + 1) +------+-----+-----+------+------+-------+ | id | lft | rgt | name | path | depth | +------+-----+-----+------+------+-------+ | 2 | 2 | 3 | A | A | 0 | | 3 | 4 | 17 | B | B | 0 | | 4 | 5 | 10 | B1 | B/B1 | 1 | | 7 | 11 | 16 | B2 | B/B2 | 1 | | 10 | 18 | 25 | C | C | 0 | | 14 | 26 | 27 | D | D | 0 | +------+-----+-----+------+------+-------+ =========Open B and B/B1========== SELECT node.id , node.lft , node.rgt , node.name , GROUP_CONCAT(parent.name ORDER BY parent.lft SEPARATOR "/" ) AS path , (COUNT(parent.lft) - 1) AS depth FROM nested_set AS node inner join nested_set AS parent on node.lft BETWEEN parent.lft AND parent.rgt where parent.lft > 1 GROUP BY node.id having depth = 0 or ('B' = left(path, length('B')) and depth = length('B') - length(replace('B', '/', '')) + 1) or ('B/B1' = left(path, length('B/B1')) and depth = length('B/B1') - length(replace('B/B1', '/', '')) + 1) +------+-----+-----+------+-----------+-------+ | id | lft | rgt | name | path | depth | +------+-----+-----+------+-----------+-------+ | 2 | 2 | 3 | A | A | 0 | | 3 | 4 | 17 | B | B | 0 | | 4 | 5 | 10 | B1 | B/B1 | 1 | | 5 | 6 | 7 | B1.1 | B/B1/B1.1 | 2 | | 6 | 8 | 9 | B1.2 | B/B1/B1.2 | 2 | | 7 | 11 | 16 | B2 | B/B2 | 1 | | 10 | 18 | 25 | C | C | 0 | | 14 | 26 | 27 | D | D | 0 | +------+-----+-----+------+-----------+-------+ =========Open B and B/B1 and C========== SELECT node.id , node.lft , node.rgt , node.name , GROUP_CONCAT(parent.name ORDER BY parent.lft SEPARATOR "/" ) AS path , (COUNT(parent.lft) - 1) AS depth FROM nested_set AS node inner join nested_set AS parent on node.lft BETWEEN parent.lft AND parent.rgt where parent.lft > 1 GROUP BY node.id having depth = 0 or ('B' = left(path, length('B')) and depth = length('B') - length(replace('B', '/', '')) + 1) or ('B/B1' = left(path, length('B/B1')) and depth = length('B/B1') - length(replace('B/B1', '/', '')) + 1) or ('C' = left(path, length('C')) and depth = length('C') - length(replace('C', '/', '')) + 1) +------+-----+-----+------+-----------+-------+ | id | lft | rgt | name | path | depth | +------+-----+-----+------+-----------+-------+ | 2 | 2 | 3 | A | A | 0 | | 3 | 4 | 17 | B | B | 0 | | 4 | 5 | 10 | B1 | B/B1 | 1 | | 5 | 6 | 7 | B1.1 | B/B1/B1.1 | 2 | | 6 | 8 | 9 | B1.2 | B/B1/B1.2 | 2 | | 7 | 11 | 16 | B2 | B/B2 | 1 | | 10 | 18 | 25 | C | C | 0 | | 11 | 19 | 20 | C1 | C/C1 | 1 | | 12 | 21 | 22 | C2 | C/C2 | 1 | | 13 | 23 | 24 | C3 | C/C3 | 1 | | 14 | 26 | 27 | D | D | 0 | +------+-----+-----+------+-----------+-------+ 

Вот и все. вы просто продолжаете дублировать это или раздел для каждого пути, который вам нужно открыть.

См. http://mikehillyer.com/articles/managing-hierarchical-data-in-mysql/, если вам нужна общая информация о работе с вложенными наборами в MySQL.

Дайте знать, если у вас появятся вопросы.

НТН,

-Dipin

Хорошая статья о том, как построить вложенный набор с нуля, который написал друг, здесь; Вложенный набор в MySQL

Возможно, это полезно для вас.

Я понимаю, что это может быть старым вопросом, но когда я наткнулся на одну и ту же проблему, я решил внести определенный вклад, чтобы другие могли принести пользу.

-Dipins ответ был тот, на котором я основывал свой прогресс, теперь я думаю, что у меня есть решение без всех «OR».

Просто замените часть:

 HAVING depth = 1 OR '".$path."' LIKE CONCAT(SUBSTRING(path, 1, (LENGTH(path) - LENGTH(menu_node_name) -1)), '%') $path = requested path. parent node's path that the user clicked, "A/B" for example path = the path of the current node including the nodes name "A/B/B1" for example, which is a child for the node the user clicked. menu-node-name = the name of the node in progress, "B1" for example. 

Что он делает, сравнивает запрошенный путь, скажем, A / B / B1. Путь к узлу. Путь узла требовал некоторой работы. LIKE path-of-node% действительно работал, но он только дал верхний уровень и не дал никаких других узлов на одном уровне. Эта версия делает.

WE конкатенирует path_of_node с подстановочным знаком (%), что означает, что после него все может прийти. Подстрока УДАЛЯЕТ имя собственного узла и тире, создавая путь path_of_node на самом деле на пути его родительского узла. Таким образом, A / B / B1 становится «A / B%», который соответствует нашему запросу, если мы нажмем ссылку, чтобы открыть новое поддерево.

Причина, по которой у меня есть глубина = 1, состоит в том, что у меня может быть несколько меню в одном и том же дереве, и я не хочу, чтобы люди видели что-то вроде «МЕНЮ-ДЛЯ-БОЛЬШОЙ ЛЮДЕЙ», «МЕНЮ-ДЛЯ-ЛЮДЕЙ», или что-то еще имена в любом случае. Узлы верхнего уровня моего набора являются дочерними узлами, я исключаю их из фактического результата.

Надеюсь, это окажется полезным для кого-то, по крайней мере, я искал решение в течение нескольких часов, а затем придумал его.

я думаю, за несколько дней вы можете подтвердить, что это сработало, посмотрев на http://www.race.fi

EDIT / ПРИМЕЧАНИЕ:

Я проверил еще несколько, и кажется, что заказ был ошибочным. Вот быстрая копия моего запроса с правильным порядком. Есть некоторые ненужные вещи, такие как локали, контент и content_localised, но ключевые моменты должны быть ясными.

 SELECT REPEAT('-',(COUNT(MENU.par_name) - 2)) as indt, GROUP_CONCAT(MENU.par_name ORDER BY MENU.par_lft SEPARATOR '/' ) AS path, (COUNT(MENU.par_lft) - 1) AS depth, MENU.*, MENU.content FROM (SELECT parent.menu_node_name AS par_name, parent.lft AS par_lft, node.menu_node_id, node.menu_node_name, node.content_id, node.node_types, node.node_iprop, node.node_aprop, node.node_brands, node.rgt, node.lft, [TPF]content_localised.content FROM [TPF]" . $this->nestedset_table . " AS node JOIN [TPF]" . $this->nestedset_table . " AS parent ON node.lft BETWEEN parent.lft AND parent.rgt JOIN [TPF]content ON node.content_id = [TPF]content.content_id JOIN [TPF]content_localised ON [TPF]content.content_id = [TPF]content_localised.content_id JOIN [TPF]locales ON [TPF]content_localised.locale_id = [TPF]locales.locale_id ORDER BY node.rgt, FIELD(locale, '" . implode("' , '", $locales) . "', locale) ASC ) AS MENU GROUP BY MENU.menu_node_id HAVING depth = 1 OR '".$path."' LIKE CONCAT(SUBSTRING(path, 1, (LENGTH(path) - LENGTH(MENU.menu_node_name) -1)), '%') AND depth > 0 ORDER BY MENU.lft"; 

Поместит ли он в объем вашего проекта просто скрытие нежелательных элементов? например (css):

  • .menu li> ul {display: none;}
  • .menu li.clicked> ul {display: block;}

Затем используйте javascript, чтобы добавить класс «нажал» на любой элемент <li>, который был нажат. Обратите внимание, что этот CSS не будет работать в IE6.