Arduino 怎么写多级菜单

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Arduino 怎么写多级菜单相关的知识,希望对你有一定的参考价值。

本帖最后由 qhdtc5 于 2015-3-25 08:00 编辑

说明:
使用了LCD5110作为显示屏,库为LCD5110_Basic
借鉴了其他程序的一些思路,用链表作为菜单结构,并使用Action概念来确定菜单项的动作
虽然动作类型设想有好几种,但我只写了设置逻辑值的菜单动作,其他的可自行完善(我也会继续写完)
代码未优化和未考虑内存占用等问题
请大家指教

先上图片和视频

[media]http://player.youku.com/player.php/sid/XOTE5MjkyOTg4/v.swf[/media]

代码:
[mw_shl_code=bash,true]#include <Wire.h>

#include <DS3231.h>
#include <LCD5110_Basic.h>

#define LIGHT_PIN 3
#define LED_PIN 13

// 菜单最大显示行数
#define MENU_MAX_ROW 5

/**
* 以下定义菜单项类型
*/
// 具有子菜单的菜单项
#define MENU_SUBMENU 0
// 参数项(用于执行参数设置)
#define MENU_PARAM 1
// 无动作的菜单项
#define MENU_ACTION_NONE 101
// 执行逻辑参数设置的菜单项(开/关、真/假等设置)
#define MENU_ACTION_LOGIC 102
// 执行数值调整设置的菜单项(时间、音量等设置)
#define MENU_ACTION_NUMBER 103
// 执行字符串设置的菜单项(欢迎语、LED显示文字等设置)
#define MENU_ACTION_STRING 104

/**
* 以下定义按键引脚
* 设置上拉电阻,低电平有效
*/
#define KEY_UP 7
#define KEY_DOWN 6
#define KEY_ENTER 5
#define KEY_EXIT 4

// 定义按键消抖延时时间
#define KEY_TREMBLE_TIME 20

// DS3231时钟变量
DS3231 clock;
// LCD5110液晶屏变量
LCD5110 myGLCD(8, 9, 10, 11, 12);
// LCD5110使用的字体
extern unsigned char SmallFont[];

/**
* 菜单结构
* 一个数组既代表一级菜单,数组中的一个元素就是一个菜单项,就是一个单独的结构体,
* 数组中的第一个元素中的num值很重要,表示本级菜单具有多少个菜单项。
*
* @var int num 本级菜单数量,必须在第一项中设置正确的菜单项数量
* @var char* label 菜单文本
* @var int type 此项类型,参考宏定义
* @var void (*action)(const char *) 指向动作的函数指针,此项要执行的具体动作函数
* @var mymenu* next 下一级菜单,只需在菜单的第一项设置,其他项置空(NULL)即可
* @var mymenu* prev 上一级菜单,同上
*/
struct mymenu
int num;
char *label; //display label
int type; //0:item, 1:submenu, 2:param
void (*action)(const char *);
mymenu * next;
mymenu * prev;
;

/**
* 逻辑参数设置菜单
* 逻辑菜单表示菜单项中的action函数要执行逻辑设置动作,即设置某个参数的逻辑值
* 逻辑菜单只需两个菜单项代表true和false
* 需要遵守的规则为:菜单中的文本需要设置为“ON”和“OFF”,
* 在执行动作函数的时候,可以将正确的参数传递过去,
* 动作函数的规则参照函数说明
*/
mymenu logic_menu[2] =
2, "ON", MENU_PARAM, NULL, NULL, NULL,
2, "OFF", MENU_PARAM, NULL, NULL, NULL
;

/**
* 下面定义了三级菜单说明了菜单应该如何个定义
*/
// 第二级菜单
mymenu light_menu[2] =
//第一项什么也不做,所以设置了类型为MENU_ACTION_NONE
2, "light 1", MENU_ACTION_NONE, NULL, NULL, NULL,
//第二项指向了下级菜单,所以设置了类型为MENU_SUBMENU
2, "light submenu", MENU_SUBMENU, NULL, NULL, NULL
;
// 第三级菜单
mymenu test_level3_menu[7] =
7, "level3menu-1", MENU_ACTION_NONE, NULL, NULL, NULL,
7, "level3menu-2", MENU_ACTION_NONE, NULL, NULL, NULL,
7, "level3menu-3", MENU_ACTION_NONE, NULL, NULL, NULL,
7, "level3menu-4", MENU_ACTION_NONE, NULL, NULL, NULL,
//定义了一个逻辑动作,这里是控制PIN13脚的LED
7, "LED TEST", MENU_ACTION_LOGIC, NULL, NULL, NULL,
7, "level3menu-6", MENU_ACTION_NONE, NULL, NULL, NULL,
7, "level3menu-7", MENU_ACTION_NONE, NULL, NULL, NULL
;
// 第一级菜单
mymenu main_menu[4] =
4, "item 1", MENU_ACTION_NONE, NULL, NULL, NULL,
4, "item 2", MENU_ACTION_NONE, NULL, NULL, NULL,
//指向下一级菜单
4, "item 2.1", MENU_SUBMENU, NULL, NULL, NULL,
4, "item 3", MENU_ACTION_NONE, NULL, NULL, NULL
;

// 定义菜单操作需要的全局变量
// 分别为当前菜单、上一级菜单、当前菜单项索引和开始显示的菜单项索引
mymenu *cur_item, *prev_item;
int item_index, start_index;

bool stat;

/**
* DS3231需要的全局变量
*/
bool Century=false;
bool h12;
bool PM;
byte ADay, AHour, AMinute, ASecond, ABits;
bool ADy, A12h, Apm;
int second,minute,hour,date,month,year,temperature;
int oldsecond; //此变量用于判断是否更新显示内容

// 定义LCD背光显示计时变量,无按键操作超时就关闭背光
unsigned long starttime;
// 定义菜单的操作按键(上、下、进入和返回)状态变量
bool old_up_stat, old_down_stat, old_enter_stat, old_exit_stat;

void setup()
//设置LCD背光引脚为输出
pinMode(LIGHT_PIN, OUTPUT);
//设置按键为上拉电阻输入(低电平有效)
for (int i=4; i<=7; i++)
pinMode(i, INPUT_PULLUP);

//初始化时打开LCD背光
backlight("ON");
stat = false;
//初始化LCD5110
myGLCD.InitLCD();
myGLCD.setContrast(127);
myGLCD.setFont(SmallFont);
myGLCD.print("Initialize...", 0, 0);

/**
* 菜单的进一步设置
* 在这里将每一个菜单的关联设置好
* 对照每一个初始设置仔细填写它们之间的关系
*/
//第一级(main_menu)的第三项指向了下一级菜单(light_menu)
main_menu[2].next = light_menu;

//第二级(light_menu)的上一级(main_menu)
light_menu[0].prev = main_menu;

//第二级(light_menu)的第二项指向了下一级菜单(test_level3_menu)
light_menu[1].next = test_level3_menu;

//第三级(test_level3_menu)的上一级(light_menu)
test_level3_menu[0].prev = light_menu;

//第三级(test_level3_menu)的第五项定义了个逻辑动作
test_level3_menu[4].action = ledtest;

/**
* 初始化当前菜单为第一级(main_menu)
*/
cur_item = main_menu;
/**
* 上一级菜单为空
*/
prev_item = NULL;
/**
* 当前选择了第一项
*/
item_index = 0;
/**
* 从第一项开始显示菜单
*/
start_index = 0;

/**
* 设置DS3231的日期和时间,只需在需要调整的时候执行一次
*/
//clock.setClockMode(true);

//I2C总线库初始化
Wire.begin();

//一些状态变量的初始化
oldsecond = -1;
old_up_stat = HIGH;
old_down_stat = HIGH;
old_enter_stat = HIGH;
old_exit_stat = HIGH;

//为了视频中能看清楚,延时定义了3秒
delay(3000);
//所有初始化完成,清屏并关闭LCD背光
myGLCD.clrRow(0);
backlight("OFF");

starttime = millis();


void loop()
//获取运行时间
unsigned long endtime = millis();
//获取当前菜单的项目数量
int menu_num = cur_item[0].num;
//定义一个临时变量
int idx;

//绘制菜单
renderMenu(&cur_item[0], menu_num>MENU_MAX_ROW ? MENU_MAX_ROW : menu_num);

//读取按键
int k_down = digitalRead(KEY_DOWN);
int k_up = digitalRead(KEY_UP);
int k_enter = digitalRead(KEY_ENTER);
int k_exit = digitalRead(KEY_EXIT);

//计算无按键时间,决定是否关闭LCD背光
if (endtime-starttime>5000)
//myGLCD.enableSleep();
backlight("OFF");


//有键按下,打开LCD背光,计时清零
if (k_up==LOW || k_down==LOW || k_enter==LOW || k_exit==LOW)
//myGLCD.disableSleep();
backlight("ON");
starttime = endtime;


//检测按键,进入相应动作
if (k_up == LOW && k_up!=old_up_stat)
//消抖
delay(KEY_TREMBLE_TIME);
if (k_up == LOW)
item_index --;

else if (k_down == LOW && k_down!=old_down_stat)
//消抖
delay(KEY_TREMBLE_TIME);
if (k_down == LOW)
item_index ++;

else if (k_enter == LOW && k_enter!=old_enter_stat)
//消抖
delay(KEY_TREMBLE_TIME);
if (k_enter == LOW)
//计算此时的菜单项索引值
idx = start_index+item_index;
if (cur_item[idx].next != NULL && cur_item[idx].type == MENU_SUBMENU)
//条件成立说明此菜单项指向了下一级菜单
//此级菜单变成了上一级菜单
prev_item = cur_item;
//将指向的下一级菜单设置为当前菜单
cur_item = cur_item[idx].next;
//重置菜单项索引和绘制索引
item_index = 0;
start_index = 0;
//清屏
myGLCD.clrScr();
else if (cur_item[idx].action != NULL && cur_item[idx].type != MENU_PARAM)
//条件成立说明此项菜单是动作
//此级菜单变成上一级菜单
prev_item = cur_item;
//根据动作类型调用相应的下一级菜单
switch(cur_item[idx].type)
case MENU_ACTION_LOGIC:
//将动作函数传递给逻辑菜单,使逻辑菜单能够正确执行动作
logic_menu[0].action = cur_item[idx].action;
//设置当前菜单为逻辑菜单
cur_item = logic_menu;
//重置菜单项索引和绘制索引
item_index = 0;
start_index = 0;
break;
case MENU_ACTION_NUMBER:
break;
case MENU_ACTION_STRING:
break;
default:
break;

//清屏
myGLCD.clrScr();
else if (cur_item[idx].type == MENU_PARAM)
//条件成立说明正在执行动作
//调用相应的动作函数,并传递参数(这里只举例逻辑设置)
cur_item[0].action((const char *)cur_item[idx].label);


else if (k_exit == LOW && k_exit!=old_exit_stat)
//消抖
delay(KEY_TREMBLE_TIME);
if (k_exit == LOW)
//返回上一级菜单的操作
if (prev_item != NULL)
//设置上一级菜单为当前菜单
cur_item = prev_item;
//设置当前菜单的上一级菜单
prev_item = cur_item[0].prev;
//重置菜单项索引和绘制索引
item_index = 0;
start_index = 0;
//清屏
myGLCD.clrScr();




/**
* 菜单项上下选择是否越界
*/
if (item_index<0)
item_index = 0;
start_index --;
if (start_index<0) start_index = 0;

if (item_index>=MENU_MAX_ROW || item_index>=menu_num)
if (item_index>=menu_num) item_index = menu_num-1;
if (item_index>=MENU_MAX_ROW) item_index = MENU_MAX_ROW-1;
if (start_index+MENU_MAX_ROW<menu_num) start_index ++;


//保存按键状态
old_up_stat = digitalRead(KEY_UP);
old_down_stat = digitalRead(KEY_DOWN);
old_enter_stat = digitalRead(KEY_ENTER);
old_exit_stat = digitalRead(KEY_EXIT);

//renderClock(0, 0);


/**
* 关闭LCD背光
* 此函数符合菜单动作定义规则,即无返回值、一个字符串参数
*
* @var const char* stat 值为“ON"打开背光,为“OFF”关闭背光
*/
void backlight(const char *stat)

//这里定义了一个逻辑动作
//根据参数决定LCD背光的开关
digitalWrite(LIGHT_PIN, strcmp(stat,"ON") ? LOW : HIGH);


/**
* 逻辑动作测试函数
*
* @var const char* stat 根据此参数来执行动作,这里测试PIN13的LED
*/
void ledtest(const char *stat)

//根据参数决定LED开关
digitalWrite(LED_PIN, strcmp(stat,"ON") ? LOW : HIGH);


/**
* 绘制单个菜单项
*
* @var struct mymenu item 要绘制的项目
* @var int row 绘制坐标(LCD5110绘制文字时只需一个行坐标即可,可根据实际显示设备调整)
* @var bool rev 是否反相绘制,默认为不反相绘制(用于当前选中项绘制,可根据实际显示设备调整)
*/
void renderItem(struct mymenu item, int row, bool rev = false)

char label[15];
//规范显示项目文字(左对齐,补空格)
sprintf(label, "%-14s", item.label);
if (rev)
//打开5110的反相绘制功能
myGLCD.invertText(true);

//绘制菜单项
myGLCD.print(label, 0, row);
if (rev)
//关闭5110的反相绘制功能
myGLCD.invertText(false);



/**
* 绘制某一级菜单
*
* @var struct mymenu* items 需要绘制的菜单
* @var int menu_num 菜单项目数量
*/
void renderMenu(struct mymenu *items, int menu_num)

//绘制数量不能超过每一屏的最大绘制数量
int num = menu_num>MENU_MAX_ROW ? MENU_MAX_ROW : menu_num;

for (int i=0; i<num; i++)
//绘制每个菜单项
renderItem(items[i+start_index], i*8, i==item_index ? true:false);



void renderClock(int row)

char s[12];
second = clock.getSecond();
minute = clock.getMinute();
hour = clock.getHour(h12, PM);
date = clock.getDate();
month = clock.getMonth(Century);
year = clock.getYear();
temperature = clock.getTemperature();
if (oldsecond!=second)
sprintf(s, "20%02d-%02d-%02d", year, month, date);
myGLCD.print(s, 0, row);
sprintf(s, "%02d:%02d:%02d", PM==0 ? hour:hour+12, minute, second);
myGLCD.print(s, 0, row+8);
sprintf(s, "%3d", sizeof(main_menu)/sizeof(mymenu));
myGLCD.print(s, 0, row+16);
oldsecond = second;



void clockSetting(int year, int month, int day, int hour, int minute, int second)
clock.setSecond(second);//配置秒
clock.setMinute(minute
参考技术A

arduino写多级菜单 ,还是看实例吧,我上传到了附件,希望 能帮助到你。


Vue 里,多级菜单要如何设计才显得专业?

老生常谈了!

虽然我们是 Java 猿,但是写起来前端代码也不含糊!今天我想来和大家聊聊这个前端的动态菜单,要如何设计才显得专业!还是以我们的 TienChin 项目为例,大家一起来看看。

先来一张截图看看效果:

那么这样的菜单是如何设计出来的呢?

今天我也不想和大家聊过多的技术细节,就聊聊这个路由是如何设计的,一旦大家明白了路由是如何设计的,剩下的问题都是细枝末节的问题了。

1. 路由设计

有的小伙伴做过 vhr,知道 vhr 里的动态菜单实现方式,松哥和大家一样,也是在不断学习不断进步中,今天我想和大家探讨 TienChin 项目中动态菜单的实现方案,看看是否是一种更佳的解决方案。

1.1 菜单设计

先来和小伙伴们回顾下 vhr 中的方案:

在 vhr 中,权限的控制,只控制到二级菜单,也就是一级菜单和权限没关系。举个例子,现在有一级菜单 A 和 二级菜单 B,B 是 A 中的菜单,现在假设:

  • 如果当前用户权限可以查看 B 菜单,那么 A 菜单会自动显示出来。
  • 如果当前用户权限无法查看 B 菜单,且 A 菜单中也没有其他子菜单可以展示,那么 A 菜单就不会显示出来。

换言之,A 菜单显示与否,主要看它里边有没有子菜单需要展示,如果有,A 菜单就显示,如果没有,A 菜单就不显示。

vhr 中的思路是这样的。

在 TienChin 项目中,这一块有一些变化:

如果 A 中只有一个 B,那么似乎就没有必要再做一个两级菜单了,直接把 B 展示出来不就行了?用户操作也方便!

这是第一个不一样的地方。

1.2 路由数据

基于第一点,就涉及到一个问题,就是路由接口该如何设计?最主要是接口返回的数据格式应该是什么样子的?

首先有一点小伙伴们应该知道,这里的路由是一个嵌套路由,也就是一级菜单中嵌套着二级菜单。即使这个地方在展示的时候,不存在层级关系,例如上图中的促销活动,但是底层的数据结构也应该是嵌套路由。

好啦,不卖关子了,我们来看一段路由 JSON:

[
	"name": "Monitor",
	"path": "/monitor",
	"hidden": false,
	"redirect": "noRedirect",
	"component": "Layout",
	"alwaysShow": true,
	"meta": 
		"title": "系统监控",
		"icon": "monitor",
		"noCache": false,
		"link": null
	,
	"children": [
		"name": "Online",
		"path": "online",
		"hidden": false,
		"component": "monitor/online/index",
		"meta": 
			"title": "在线用户",
			"icon": "online",
			"noCache": false,
			"link": null
		
	, 
		"name": "Job",
		"path": "job",
		"hidden": false,
		"component": "monitor/job/index",
		"meta": 
			"title": "定时任务",
			"icon": "job",
			"noCache": false,
			"link": null
		
	]
, 
	"path": "/",
	"hidden": false,
	"component": "Layout",
	"children": [
		"name": "Role",
		"path": "role",
		"hidden": false,
		"component": "system/role/index",
		"meta": 
			"title": "角色管理",
			"icon": "peoples",
			"noCache": false,
			"link": null
		
	]
]

这里我举了两个菜单的例子,这两个例子比较具有代表性,这个菜单最终显示效果大概类似下面这样:

  • 系统监控
    • 在线用户
    • 定时任务
  • 角色管理

大概显示效果如上图。

接下来我就来说一下这里几个典型属性:

  1. redirect:noRedirect 表示该路由在面包屑导航中不可被点击。
  2. alwaysShow:如果这个属性设置为 false,那么当当前菜单只有一个子菜单的时候,默认情况下就只会显示子菜单,而忽略父菜单(如 1.1 小节所述),但是如果将该属性设置为 true,则无论当前菜单有几个子菜单,都会将当前菜单展示出来(这就类似于 vhr 中的效果了)。
  3. 每一个父菜单都有自己的 path,每一个 children 也有自己的 path,父菜单的 path 加上每一个 children 的 path,共同组成每一个 children 的路径。
  4. 再来看第二个角色管理这个菜单项,由于它的父菜单中只有一个子菜单项,并且父菜单中也没有 alwaysShow 属性,所以这个菜单项在最终展示的时候,就只展示里边的角色管理,父菜单则不会展示出来(正好,生成的 JSON 中也没说父菜单的名字、图标等属性)。

当然,不是说你的 JSON 这么写就自动这么显示,JSON 中的东西只是一个标记,最终怎么显示,还要看渲染:

<div v-if="!item.hidden">
  <template v-if="hasOneShowingChild(item.children,item) && (!onlyOneChild.children||onlyOneChild.noShowingChildren)&&!item.alwaysShow">
    <app-link v-if="onlyOneChild.meta" :to="resolvePath(onlyOneChild.path, onlyOneChild.query)">
      <el-menu-item :index="resolvePath(onlyOneChild.path)" :class="'submenu-title-noDropdown':!isNest">
        <item :icon="onlyOneChild.meta.icon||(item.meta&&item.meta.icon)" :title="onlyOneChild.meta.title" />
      </el-menu-item>
    </app-link>
  </template>
  <el-submenu v-else ref="subMenu" :index="resolvePath(item.path)" popper-append-to-body>
    <template slot="title">
      <item v-if="item.meta" :icon="item.meta && item.meta.icon" :title="item.meta.title" />
    </template>
    <sidebar-item
      v-for="child in item.children"
      :key="child.path"
      :is-nest="true"
      :item="child"
      :base-path="resolvePath(child.path)"
      class="nest-menu"
    />
  </el-submenu>
</div>

还有一个函数我就没有列出来了,反正我们看名字也大概知道每一个函数的含义。

大家看,这个 div 中实际上分为了两部分,上面 template 专门用来处理 children 中只有一项的情况(角色管理),具体处理方式就是把 children 拿出来显示,其他的则不考虑,具体执行的时候不一定是只有一个 children,也有可能压根就没有 children,此时直接显示 parent 即可(参考 1.3 小节)。

下面的 el-submenu 则处理 children 有多个的情况(系统监控)。

另外这里涉及到了一个 resolvePath,也是特别关键的一个方法,我们来大致看下:

resolvePath(routePath, routeQuery) 
  if (isExternal(routePath)) 
    return routePath
  
  if (isExternal(this.basePath)) 
    return this.basePath
  
  if (routeQuery) 
    let query = JSON.parse(routeQuery);
    return  path: path.resolve(this.basePath, routePath), query: query 
  
  return path.resolve(this.basePath, routePath)

这个函数的主要左右,就是处理菜单的路径问题。

我们来看下这个具体的判断逻辑:

  1. 如果这个菜单的路径是一个外链(判断逻辑是查看这个 path 是否有 http 或者 https 等前缀),即 isExternal 返回 true,就把这个路径原封不动返回。
  2. 如果这个菜单的父菜单的路径是一个外链,则将父菜单的 path 原封不懂返回。
  3. 如果有查询参数,就把参数加上。
  4. 最后通过 path.resolve 对路径进行一个简单运算。

有的小伙伴可能对 path.resolve 不熟悉,我简单说下:

path.resolve() 方法可以将多个路径解析为一个规范化的绝对路径,它的处理方式类似于对这些路径逐一进行 cd 操作,然而与 cd 操作不同的是,这些路径可以是文件,并且可不必实际存在(resolve() 方法不会利用底层的文件系统判断路径是否存在,而只是进行路径字符串操作)。例如:

path.resolve('foo/bar', '/tmp/file/', '..', 'a/../subfile')

相当于:

cd foo/bar
cd /tmp/file/
cd ..
cd a/../subfile
pwd

举个简单的例子:

path.resolve('/foo/bar', './baz') 
// 输出结果为 
'/foo/bar/baz' 

path.resolve('/foo/bar', '/tmp/file/') 
// 输出结果为 
'/tmp/file' 

path.resolve('wwwroot', 'static_files/png/', '../gif/image.gif') 
// 当前的工作路径是 /home/javaboy/node,则输出结果为 
'/home/javaboy/node/wwwroot/static_files/gif/image.gif'

现在大家知道菜单跳转的路径是怎么来的了吧!

1.3 外链问题

在 TienChin 项目中,菜单还存在一个外链的问题。

这个外链有两种不同的显示思路:

  1. 点击外链,直接打开一个新的选项卡,在新的选项卡中展示新的页面。
  2. 点击外链,在当前项目中打开一个新的选项卡,选项卡中展示新的内容。

对于第一种情况我就不和大家演示了,对于第二种情况,我截个图给大家看下:

就是在当前项目的选项卡中,展示一个外部链接的内容。

我们先来看第一种情况。即点击菜单之后,就在一个新的选项卡中打开网页,这种菜单的 JSON 格式如下:


    "name": "Http://www.javaboy.org",
    "path": "http://www.javaboy.org",
    "hidden": false,
    "component": "Layout",
    "meta": 
        "title": "TienChin健身官网",
        "icon": "guide",
        "noCache": false,
        "link": "http://www.javaboy.org"
    

这个大家看,也没有 children,因为不需要,这个显示的时候,就当成了只有一个 children 来处理,然后菜单项的 path 是一个 http 路径,一点击,自然就跳到新的选项卡了。

对于第二种情况,即点击外链,在当前项目中打开一个新的选项卡,选项卡中展示链接的内容,它的 JSON 结构类似下面这样:


    "name": "Http://www.javaboy.org",
    "path": "/",
    "hidden": false,
    "component": "Layout",
    "meta": 
        "title": "TienChin健身官网",
        "icon": "guide",
        "noCache": false,
        "link": null
    ,
    "children": [
        
            "name": "Www.javaboy.org",
            "path": "www.javaboy.org",
            "hidden": false,
            "component": "InnerLink",
            "meta": 
                "title": "TienChin健身官网",
                "icon": "guide",
                "noCache": false,
                "link": "http://www.javaboy.org"
            
        
    ]

这个其实也没啥好说的,类似于上面系统监控的那种情况,但是只有一个子菜单,在菜单渲染的时候,也是只渲染一个子菜单。由于父子菜单的 path 都不是以 http 或者 https 之类的地址开头,所以这个链接最终生成的 path 是 /www.javaboy.org,然后这个路径的内容将展示在 InnerLink 组件上,最终就是大家上图中所看到的效果了。

好啦,这就是前端菜单的各种情况,后端菜单如何按照需要返回数据,咱们继续~

2. 菜单表

首先我们来看看菜单表的定义,也就是 sys_menu

CREATE TABLE `sys_menu` (
  `menu_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '菜单ID',
  `menu_name` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '菜单名称',
  `parent_id` bigint(20) DEFAULT '0' COMMENT '父菜单ID',
  `order_num` int(4) DEFAULT '0' COMMENT '显示顺序',
  `path` varchar(200) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '路由地址',
  `component` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '组件路径',
  `query` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '路由参数',
  `is_frame` int(1) DEFAULT '1' COMMENT '是否为外链(0是 1否)',
  `is_cache` int(1) DEFAULT '0' COMMENT '是否缓存(0缓存 1不缓存)',
  `menu_type` char(1) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '菜单类型(M目录 C菜单 F按钮)',
  `visible` char(1) COLLATE utf8mb4_unicode_ci DEFAULT '0' COMMENT '菜单状态(0显示 1隐藏)',
  `status` char(1) COLLATE utf8mb4_unicode_ci DEFAULT '0' COMMENT '菜单状态(0正常 1停用)',
  `perms` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '权限标识',
  `icon` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT '#' COMMENT '菜单图标',
  `create_by` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '创建者',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  `update_by` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '更新者',
  `update_time` datetime DEFAULT NULL COMMENT '更新时间',
  `remark` varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '备注',
  PRIMARY KEY (`menu_id`)
) ENGINE=InnoDB AUTO_INCREMENT=3054 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='菜单权限表';

其实这里很多字段都和我们 vhr 项目项目很相似,我也就不重复啰嗦了,我这里主要和小伙伴们说一个字段,那就是 menu_type

menu_type 表示一个菜单字段的类型,一个菜单有三种类型,分别是目录(M)、菜单(C)以及按钮(F)。这里所说的目录,相当于我们在 vhr 中所说的一级菜单,菜单相当于我们在 vhr 中所说的二级菜单。

当用户从前端登录成功后,要去动态加载的菜单的时候,就查询 M 和 C 类型的数据即可,F 类型的数据不是菜单项,查询的时候直接过滤掉即可,通过 menu_type 这个字段可以轻松的过滤掉 F 类型的数据。小伙伴们想想,F 类型的数据过滤掉之后,剩下的数据不就是一级菜单和二级菜单了,那不就和 vhr 又一样了么!

在 vhr 中,考虑到菜单就是只有两级:一级菜单和二级菜单,一级菜单是目录,二级菜单是则是具体的菜单项,没有三级菜单!所以在 vhr 中,查询菜单的时候我直接用了一个一对多的查询,将一级菜单做一的一方,二级菜单做多的一方,这样比较省事。当然灵活度差一点,所以在 TienChin 项目中,这块还是用上了递归。

3. 前端菜单展示

接下来,前端菜单展示分为了几种情况?这个松哥在之前的文章中已经和大家聊过了,具体可以参考Vue 里,多级菜单要如何设计才显得专业?一文,这里不再赘述。

4. 菜单接口

当用户登录成功之后,会自动请求 /getRouters 接口来获取菜单信息,我们一起来看下:

/**
 * 获取路由信息
 *
 * @return 路由信息
 */
@GetMapping("getRouters")
public AjaxResult getRouters() 
    Long userId = SecurityUtils.getUserId();
    List<SysMenu> menus = menuService.selectMenuTreeByUserId(userId);
    return AjaxResult.success(menuService.buildMenus(menus));

这里的查询实际上分为两个步骤:

  1. 根据用户 id 查询到所有的菜单信息,这一步的查询实际上是比较容易的,就单纯的多张表联合在一起,然后过滤出和当前用户相关并且菜单类型为 M 或者 C 的菜单(类型为 F 的表示按钮,就不要了),查询到菜单信息之后,然后进行一个递归操作,将菜单数据的层级排列出来。
  2. menuService.buildMenus 这一步则是将菜单数据专为前端所需要的路由数据。

一共就这两个步骤,我们来逐一进行分析。

先来看查询菜单数据。

/**
 * 根据用户ID查询菜单
 *
 * @param userId 用户名称
 * @return 菜单列表
 */
@Override
public List<SysMenu> selectMenuTreeByUserId(Long userId) 
    List<SysMenu> menus = null;
    if (SecurityUtils.isAdmin(userId)) 
        menus = menuMapper.selectMenuTreeAll();
     else 
        menus = menuMapper.selectMenuTreeByUserId(userId);
    
    return getChildPerms(menus, 0);

/**
 * 根据父节点的ID获取所有子节点
 *
 * @param list     分类表
 * @param parentId 传入的父节点ID
 * @return String
 */
public List<SysMenu> getChildPerms(List<SysMenu> list, int parentId) 
    List<SysMenu> returnList = new ArrayList<SysMenu>();
    for (Iterator<SysMenu> iterator = list.iterator(); iterator.hasNext(); ) 
        SysMenu t = (SysMenu) iterator.next();
        // 一、根据传入的某个父节点ID,遍历该父节点的所有子节点
        if (t.getParentId() == parentId) 
            recursionFn(list, t);
            returnList.add(t);
        
    
    return returnList;

/**
 * 递归列表
 *
 * @param list
 * @param t
 */
private void recursionFn(List<SysMenu> list, SysMenu t) 
    // 得到子节点列表
    List<SysMenu> childList = getChildList(list, t);
    t.setChildren(childList);
    for (SysMenu tChild : childList) 
        if (hasChild(list, tChild)) 
            recursionFn(list, tChild);
        
    

/**
 * 得到子节点列表
 */
private List<SysMenu> getChildList(List<SysMenu> list, SysMenu t) 
    List<SysMenu> tlist = new ArrayList<SysMenu>();
    Iterator<SysMenu> it = list.iterator();
    while (it.hasNext()) 
        SysMenu n = (SysMenu) it.next();
        if (n.getParentId().longValue() == t.getMenuId().longValue()以上是关于Arduino 怎么写多级菜单的主要内容,如果未能解决你的问题,请参考以下文章

为啥arduino程序写好之后另存为到其他文件夹就会导致软件运行崩溃呢?

如何用Arduino uno r3给另一块板子烧录Bootloader

我想用arduino leonardo的串口即0脚和1脚连接XBEE传输数据,并由电脑接收,请问应该怎么实现。

arduino 怎么生存hex

怎么把arduino代码转成mixly

arduino 怎么和上位机通信