Featured image of post 微信小程序实现仿支付宝的菜单管理

微信小程序实现仿支付宝的菜单管理

前言

使用原生微信小程序的movable-area实现拖拽排序以及菜单的新增、删除、预览等,类似支付宝首页顶部应用的效果。

效果预览

menu.gif

小程序首页展示默认的9个菜单应用+更多按钮,点击更多进入所有应用页,在此页面可以对应用进行添加、删除和拖动排序,页面上方是操作区,下方是各种分类下的应用,点击编辑时仅可在操作区进行拖动排序。点击完成保存编辑结果,点击取消恢复到操作前。

实现步骤

首页应用区的静态页面实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
    <view class="category-card">
      <view class="category-title">首页应用</view>
      <!-- 非编辑模式:预览 同页面底部应用区一致 仅作展示 -->
      <block wx:if="{{!editMode}}">
        <view class="menus-container">
          <view class="single-menu" catchtap="handleIndexMenu" wx:for="{{indexMenus}}" wx:key="path" data-path="{{item.path}}" data-active="{{item.active}}">
            <image wx:if="{{!editMode}}" src="{{item.icon}}" class="menu-icon" />
            <image wx:elif="{{item.active}}" src="{{item.icon_del}}" class="menu-icon" />
            <image wx:else src="{{item.icon_add}}" class="menu-icon" />
            <view class="menu-title">{{item.name}}</view>
          </view>
        </view>
      </block>
      <block wx:else>
        <view class="container">
          <movable-area class="item_box" style="width: {{boxWidth}}rpx;height: {{boxHeight}}rpx">
            <movable-view class="item {{selectId === item.resourceId?'item_show':'item_hide'}}" wx:for="{{swipeItem}}" wx:key="path" x="{{item.x}}rpx" y="{{item.y}}rpx" direction="all" bindchange="touchMove" bindtouchend="touchend" data-index="{{item.index}}" data-id="{{item.id}}" bindtouchstart="unlockItem">
              <image wx:if="{{!editMode}}" src="{{item.icon}}" class="menu-icon" data-path="{{item.path}}" data-active="{{item.active}}" catchtap="handleIndexMenu" />
              <image wx:elif="{{item.active}}" src="{{item.icon_del}}" class="menu-icon" data-path="{{item.path}}" data-active="{{item.active}}" catchtap="handleIndexMenu" />
              <image wx:else src="{{item.icon_add}}" class="menu-icon" />
              <view class="menu-title">{{item.name}}</view>
            </movable-view>
          </movable-area>

          <view class="item_box layer_box" style="width: {{boxWidth}}rpx;height: {{boxHeight}}rpx">
            <view class="item layer_item {{selectId == item.id?'item_hide':''}}" wx:for="{{layerItem}}" wx:key="id" style="left: {{item.x}}rpx;top: {{item.y}}rpx">
              <image wx:if="{{!editMode}}" src="{{item.icon}}" class="menu-icon" />
              <image wx:elif="{{item.active}}" src="{{item.icon_del}}" class="menu-icon" />
              <image wx:else src="{{item.icon_add}}" class="menu-icon" />
              <view class="menu-title">{{item.name}}</view>
            </view>
          </view>
        </view>
      </block>
    </view>

对于编辑模式下的菜单页面设计,使用了两个图层。layerItem是没有被拖动的菜单项,作为拖动菜单的背景层;swipeItem通过数组遍历生成多个可以拖动的菜单项。当用户按住某个菜单时,隐藏固定层中的该菜单的显示。

对于拖动元素moveable-view,当用户按住某个菜单的时候会触发unlockItem,

1
2
3
4
5
  unlockItem(e) {
    this.setData({
      selectId: e.currentTarget.dataset.id,
    });
  },

滑块拖动和页面排序

计算单个菜单格子的宽度

1
2
3
4
5
6
7
8
data: {
    // 750 - 48 内边距
    boxWidth: 702, //容器宽度,100%为750,单位rpx
    boxHeight: 0, //容器高度
    height: 140, //滑块总高度,即滑块本身加上边距的高度
    selectId: 0, //当前选中滑块的id
    col: 4, //滑块列数
}
1
2
3
4
5
6
7
8
touchMove(e) {
    let {
      boxWidth,
      height,
      layerItem,
      col
    } = this.data;
    let width = Math.trunc(boxWidth / col); //每块区域的宽度

当用户手动进行拖动时,判断菜单当前到了哪个位置,修改key值

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
if (e.detail.source === "touch") {  //区别于其它原因造成的位置变化 比如:由于惯性继续移动,超出范围自动回弹等
      let arr = [...layerItem];
      let id = e.currentTarget.dataset.id;
      let centerX = e.detail.x * 2 + width / col; //当前选中滑块的中心的x坐标
      let centerY = e.detail.y * 2 + height / col; //当前选中滑块的中心的y坐标
      let key = 0; //滑块滑动时 菜单当前应该移动到的新位置
      let index = 0; //滑块滑动前 的位置

      //通过id判断当前滑块的index
      layerItem.forEach((item) => {
        if (item.id === id) {
          index = item.index;
        }
      });
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
//根据当前滑块位置确认当前所处在哪个区域
for (let i = 0; i < arr.length + 1; i++) {
        let x1 = (i % col) * (boxWidth / col); //第n个区域的左 x坐标
        let x2 = ((i % col) + 1) * (boxWidth / col); //第n个区域的右 x坐标
        let y1 = Math.trunc(i / col) * height; //第n个区域的左 y坐标
        let y2 = Math.trunc(i / col + 1) * height; //第n个区域的右 y坐标
        //判断当前滑块所属区域
        if (centerX > x1 && centerX < x2 && centerY > y1 && centerY < y2) {
          key = i;
        }
      }
1
2
3
4
5
// 当拖动到最后一个菜单之后的空白区域时,key 超过最后一个有效下标
// 比如6个格子 数组长度是5 拖动到最后一个格子 但是key应该取5
if (key >= arr.length - 1) {
    key = arr.length - 1;
}

调整菜单整体新的顺序

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
//滑动时位置与滑动前不同时
 if (index != key) {
    //计算数组中其他数据变化后的index
    arr.forEach((item, i) => {
      if (item.id != id) {
        // 其它元素向前移动
        if (index > key) {
          if (item.index >= key && item.index < index) {
            item.index = item.index + 1;
          }
        }
        // 其它元素向后移动
        if (index < key) {
          if (item.index > index && item.index <= key) {
             item.index = item.index - 1;
          }
        }
      } else {
        item.index = key;
      }
    });

根据菜单新的索引值计算元素位置,并保存数据,更新页面

1
2
3
4
5
6
7
8
arr.forEach((item, i) => {
  item.x = (item.index % col) * (boxWidth / col);
  item.y = Math.trunc(item.index / col) * height;
});

this.setData({
    layerItem: arr,
});

最后,当停止拖动时 同步两组数据

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
  /**
   * 停止拖动,两数组同步
   */
  touchend(e) {
    const arrayTemp = JSON.parse(JSON.stringify(this.data.layerItem));
    // 根据x,y值排序
    arrayTemp.sort((a, b) => {
      if (a.y === b.y) return a.x - b.x;
      return a.y - b.y;
    });
    this.setData({
      layerItem: arrayTemp,
      swipeItem: arrayTemp,
      indexMenus: arrayTemp,
    });
  },

源码链接

有帮助的话 请点击star

本博客已稳定运行
Built with Hugo
Theme Stack designed by Jimmy