导航菜单管理

导航菜单的数据会以 JSON 的方式存在 options 表中 keynav_menusvalue 中。

加载数据到表格展示

一般系统的 options 表的结构就是键值结构,也就是必然会有两个列,一个是 key,另一个是 value,这种结构比较灵活

在页面加载过后,根据配置选项的键 nav_menus 获取对应的数据(JSON):

 
1
$(function () {
2
  /**
3
   * 显示消息
4
   * @param  {String} msg 消息文本
5
   */
6
  function notify (msg) {
7
    $('.alert').text(msg).fadeIn()
8
    // 3000 ms 后隐藏
9
    setTimeout(function () {
10
      $('.alert').fadeOut()
11
    }, 3000)
12
  }
13
14
  /**
15
   * 加载导航菜单数据
16
   */
17
  function loadData () {
18
    $.get('/admin/options.php', { key: 'nav_menus' }, function (res) {
19
      if (!res.success) {
20
        // 失败,提示
21
        return notify(res.message)
22
      }
23
24
      var menus = []
25
26
      try {
27
        // 尝试以 JSON 方式解析响应内容
28
        menus = JSON.parse(res.data)
29
      } catch (e) {
30
        notify('获取数据失败')
31
      }
32
33
      // 使用 jsrender 渲染数据到表格
34
      $('tbody').html($('#menu_tmpl').render(menus))
35
    })
36
  }
37
38
  // 首次加载数据
39
  loadData()
40
})

🚩 源代码: step-76

--

新增导航菜单

思路:在点击保存按钮时,先获取全部导航菜单的数据,然后将界面上填写的数据 push 进去,然后再序列化为一个 JSON 字符串,通过 AJAX 发送到服务端保存。

名词解释:

  1. 将一个对象转换为一个 JSON 字符串的过程叫做序列化
  2. 同理将一个 JSON 字符串转换为一个对象的过程叫做反序列化

获取当前导航菜单数据

作为当前的情况,我们可以有两种方式获取当前导航菜单数据:

  1. 将之前的 menus 定义成全局成员,让其在按钮点击时可以被访问。
  2. 点击时再次发送 AJAX 请求获取最新的数据。

提问:哪一种方式跟合适?

发送异步请求

之前我们已经定义了一个加载数据的 loadData 函数,但是在这里不能共用,因为在这个函数中拿到数据过后就渲染到界面上了,而我们这里是需要这个数据做后续逻辑。

如果需要公用,则需要改造这个函数,让其返回数据,而不是使用数据。

函数的粒度问题 函数的粒度指的是在同一个函数中业务的数量。

  1. 粒度越细,公用性越好
  2. 粒度越粗,调用越方便,性能大多数越好。

返回数据的方式

如果是一个普通情况下的函数数据返回,直接使用 return 即可,但是此处我们的数据是需要 AJAX 过后才能拿到的,不能使用简单的 return 返回,即异步编程最常见的问题,必须使用回调(委托)解决。

重新封装 loadData()

 
xxxxxxxxxx
23
1
/**
2
 * 加载导航菜单数据
3
 * @param {Function} callback 获取到数据后续的逻辑
4
 */
5
function loadData (callback) {
6
  $.get('/admin/options.php', { key: 'nav_menus' }, function (res) {
7
    if (!res.success) {
8
      // 失败,提示
9
      return callback(new Error(res.message))
10
    }
11
12
    var menus = []
13
14
    try {
15
      // 尝试以 JSON 方式解析响应内容
16
      menus = JSON.parse(res.data)
17
    } catch (e) {
18
      callback(new Error('获取数据失败'))
19
    }
20
21
    callback(null, menus)
22
  })
23
}

首次加载数据时

 
xxxxxxxxxx
6
1
// 首次加载数据
2
loadData(function (err, data) {
3
  if (err) return notify(err.message)
4
  // 使用 jsrender 渲染数据到表格
5
  $('tbody').html($('#menu_tmpl').render(data))
6
})

🚩 源代码: step-77

 
xxxxxxxxxx
14
1
/**
2
 * 新增逻辑
3
 */
4
$('.btn-save').on('click', function () {
5
  // 获取当前的菜单数据
6
  loadData(function (err, data) {
7
    if (err) return notify(err.message)
8
9
    console.log(data)
10
  })
11
12
  // 阻止默认事件
13
  return false
14
})

保存数据逻辑

封装保存数据函数

 
xxxxxxxxxx
15
1
/**
2
 * 保存导航菜单数据
3
 * @param  {Array}   data      需要保存的数据
4
 * @param  {Function} callback 保存后需要执行的逻辑
5
 */
6
function saveData (data, callback) {
7
  $.post('/admin/options.php', { key: 'nav_menus', value: JSON.stringify(data) }, function (res) {
8
    if (!res.success) {
9
      return callback(new Error(res.message))
10
    }
11
12
    // 成功
13
    callback(null)
14
  })
15
}

实现保存逻辑:

 
xxxxxxxxxx
46
1
/**
2
 * 新增逻辑
3
 */
4
$('.btn-save').on('click', function () {
5
  var menu = {
6
    icon: $('#icon').val(),
7
    text: $('#text').val(),
8
    title: $('#title').val(),
9
    link: $('#link').val()
10
  }
11
12
  // 数据校验
13
  for (var key in menu) {
14
    if (menu[key]) continue
15
    notify('完整填写表单')
16
    return false
17
  }
18
19
  // 获取当前的菜单数据
20
  loadData(function (err, data) {
21
    if (err) return notify(err.message)
22
23
    // 将界面上的数据追加到已有数据中
24
    data.push(menu)
25
26
    // 保存数据到服务端
27
    saveData(data, function (err) {
28
      if (err) return notify(err.message)
29
      // 再次加载
30
      loadData(function (err, data) {
31
        if (err) return notify(err.message)
32
        // 使用 jsrender 渲染数据到表格
33
        $('tbody').html($('#menu_tmpl').render(data))
34
35
        // 清空表单
36
        $('#icon').val('')
37
        $('#text').val('')
38
        $('#title').val('')
39
        $('#link').val('')
40
      })
41
    })
42
  })
43
44
  // 阻止默认事件
45
  return false
46
})

删除导航菜单

绑定删除按钮事件

将模板中每一个删除按钮调整为:

 
xxxxxxxxxx
1
1
<a class="btn btn-danger btn-xs btn-delete" href="javascript:;" data-index="{{: #index }}">删除</a>

为所有 btn-delete 添加点击事件:

 
xxxxxxxxxx
4
1
// 删除按钮是后续创建的所以不能直接绑定事件,这里使用委托事件
2
$('tbody').on('click', '.btn-delete', function () {
3
  // TODO: ...
4
})

思路也是获取已有数据,在已有数据中找到当前数据并移除

 
xxxxxxxxxx
23
1
/**
2
 * 删除指定数据
3
 */
4
$('tbody').on('click', '.btn-delete', function () {
5
  var index = parseInt($(this).parent().parent().data('index'))
6
7
  // 获取当前的菜单数据
8
  loadData(function (err, data) {
9
    if (err) return notify(err.message)
10
11
    data.splice(index, 1)
12
13
    // 保存数据到服务端
14
    saveData(data, function (err) {
15
      if (err) return notify(err.message)
16
      // 再次加载
17
      loadData(function (err, data) {
18
        if (err) return notify(err.message)
19
        $('tbody').html($('#menu_tmpl').render(data))
20
      })
21
    })
22
  })
23
})

思考:这样处理是否会有问题,为什么,如何解决