微信小程序 自定义组件 标签管理

在这里插入图片描述

环境

小程序环境:

  • 微信开发者工具:RC 1.06.2503281 win32-x64

  • 基础运行库:3.8.1

概述

基础功能

  • 标签增删改查:支持添加/删除单个标签、批量删除、重置默认标签

  • 数据展示:通过对话框展示结构化数据并支持复制

  • 动画系统:添加/删除时带缩放淡入淡出动画

结构分析(WXML)

数据展示优化

  • 使用scroll-view应对长内容

  • user-select开启文本选择

<!--pages/tabs/tabs.wxml-->
<navigation-bar title="标签管理" back="{{false}}" color="black" background="#FFF"></navigation-bar>

<view class="container">
  <!-- 新增输入框区域 -->
  <view class="input-area">
    <input 
      class="tag-input"
      placeholder="请输入新标签"
      value="{{inputValue}}"
      bindinput="handleInput"
      bindconfirm="handleAdd"
    />
    <button 
      size="mini"
      class="add-btn"
      bind:tap="handleAdd"
    >添加标签</button>
  </view>

  <!-- 标签容器 -->
  <view class="tag-container" wx:if="{{tags.length}}">
    <view 
      wx:for="{{tags}}"
      wx:key="id"
      class="tag-item"
      data-index="{{index}}"
      animation="{{item.animation}}"
    >
      {{item.name}}
      <view 
        class="close-btn" 
        bind:tap="handleClose"
        data-id="{{item.id}}"
      >×</view>
    </view>
  </view>

  <button class="delete-all-btn" bind:tap="handleDeleteAll">删除全部标签</button>
  <button class="reset-btn" bind:tap="handleReset">重置所有标签</button>
  <button class="reset-btn" bind:tap="handleGetAll">获取数据</button>
</view>

<mp-dialog title="获取的数据" show="{{dialogShow}}" bindbuttontap="dialogButton" buttons="{{buttons}}">
  <view class="title">
    <scroll-view scroll-y="true" class="scroll">
      <text user-select="true">{{tags_data}}</text>
    </scroll-view>
  </view>
</mp-dialog>

逻辑分析(JS)

数据管理

  • ID生成:自增ID保证唯一性

  • 数据持久化:标签数据仅存内存,页面关闭后丢失

  • 数据结构:{ id: number, name: string, animation: any }

交互设计

  • 输入验证:非空校验 + 长度限制(15字)

  • 防误操作:删除全部需二次确认

  • 视觉反馈:按钮点击态(opacity变化)、关闭按钮悬浮效果

// pages/tabs/tabs.js
Page({
  data: {
    tags: [],
    nextId: 1,
     // 输入数据字段
    inputValue: '',
    // 标签数据
    tags_data: null,
    // 显示/隐藏 对话框
    dialogShow: false,
    // 对话框按钮组
    buttons: [{
      text: '关闭'
    }, {
      text: '复制'
    }],
  },

  onLoad() {
    this.handleReset()
  },

  // 输入框内容变化
  handleInput(e) {
    this.setData({
      inputValue: e.detail.value.trim() // 自动去除首尾空格
    });
  },

  // 添加新标签
  handleAdd() {
    const newTag = this.data.inputValue;
    
    // 验证输入内容
    if (!newTag) {
      wx.showToast({
        title: '请输入标签内容',
        icon: 'none'
      });
      return;
    }

    if (newTag.length > 15) {
      wx.showToast({
        title: '请不要输入过长的内容',
        icon: 'none',
        duration: 1500
      });
      return;
    }
    
    // 创建动画
    const animation = wx.createAnimation({
      duration: 400,
      timingFunction: 'ease-out'
    });
    
    // 初始状态(隐藏状态)
    animation.opacity(0).scale(0.5).step();
    
    // 生成新标签
    const newItem = {
      id: this.data.nextId++,
      name: newTag,
      animation: animation.export() // 导出动画对象
    };
    
    // 先添加带动画的数据
    this.setData({
      tags: [...this.data.tags, newItem],
      inputValue: ''
    }, () => {
      // 在回调中执行显示动画
      setTimeout(() => {
        const index = this.data.tags.length - 1;
        const showAnimation = wx.createAnimation({
          duration: 400,
          timingFunction: 'ease-out'
        });
        showAnimation.opacity(1).scale(1).step();
        
        this.setData({
          [`tags[${index}].animation`]: showAnimation.export()
        });
        
        // 动画完成后清除动画数据
        setTimeout(() => {
          this.setData({
            [`tags[${index}].animation`]: null
          });
        }, 400);
      }, 50); // 短暂延迟确保初始状态渲染
    });
  },

  // 关闭标签(带动画)
  handleClose(e) {
    const id = parseInt(e.currentTarget.dataset.id);
    const index = this.data.tags.findIndex(item => item.id === id);
    
    // 创建动画
    const animation = wx.createAnimation({
      duration: 400,
      timingFunction: 'ease'
    });
    animation.opacity(0).scale(0.8).step();
    
    // 先执行动画
    this.setData({
      [`tags[${index}].animation`]: animation.export()
    });

    // 动画完成后移除元素
    setTimeout(() => {
      const newTags = this.data.tags.filter(item => item.id !== id);
      this.setData({ tags: newTags });
    }, 400);
  },

  // 从后往前依次删除标签
  handleDeleteAll() {
    if (this.data.tags.length === 0) {
      wx.showToast({
        title: '没有可删除的标签',
        icon: 'none'
      });
      return;
    }

    wx.showModal({
      title: '提示',
      content: '确定要删除所有标签吗?',
      success: (res) => {
        if (res.confirm) {
          this.deleteOneByOne(this.data.tags.length - 1);
        }
      }
    });
  },

  // 递归方法:从最后一个开始逐个删除
  deleteOneByOne(index) {
    if (index < 0) {
      return; // 所有标签已删除完毕
    }

    const animation = wx.createAnimation({
      duration: 200,
      timingFunction: 'ease'
    });
    animation.opacity(0).scale(0.8).step();
    
    this.setData({
      [`tags[${index}].animation`]: animation.export()
    }, () => {
      setTimeout(() => {
        // 删除当前标签
        const newTags = [...this.data.tags];
        newTags.splice(index, 1);
        this.setData({ tags: newTags }, () => {
          // 继续删除前一个标签
          this.deleteOneByOne(index - 1);
        });
      }, 200); // 等待动画完成
    });
  },

  // 重置所有标签
  handleReset() {
    this.setData({nextId: 0})
    this.initTags(['Vue', 'React', 'Angular', 'Flutter', 'UniApp', 'Taro', 'CSDN', '奇妙方程式']);
  },

  // 初始化标签方法
  initTags(labels) {
    console.log('labels', labels);
    const tags = labels.map((text, index) => ({
      id: this.data.nextId++,
      name: text,
      animation: null
    }));
    
    this.setData({ tags }, () => {
      // 在回调中逐个添加动画
      tags.forEach((_, index) => {
        setTimeout(() => {
          this.animateTag(index);
        }, index * 100); // 每个标签间隔100ms
      });
    });
  },

  // 执行单个标签的动画
  animateTag(index) {
    const animation = wx.createAnimation({
      duration: 200,
      timingFunction: 'ease-out'
    });
    animation.opacity(0).scale(0.5).step();
    animation.opacity(1).scale(1).step();
    
    this.setData({
      [`tags[${index}].animation`]: animation.export()
    });

    setTimeout(() => {
      this.setData({
        [`tags[${index}].animation`]: null
      });
    }, 200);
  },

  // 获取数据
  handleGetAll() {
    let tags = this.data.tags.map(item => ({
      id: item.id,
      name: item.name 
    }))
    console.log('获取的数据', tags);
    let tags_data = "tags: " + JSON.stringify(tags)
    this.setData({
      dialogShow: true,
      tags_data
    })
  },

  // 对话框 按钮
  dialogButton(e) {
    this.setData({
      dialogShow: false,
    })

    let choose = e.detail.item.text
    if (choose == "复制") {
      this.copy()
    }
  },

  // 复制 参数信息
  copy() {
    wx.setClipboardData({
      data: this.data.tags_data,
      success() {
        wx.getClipboardData({
          success() {
            wx.showToast({
              title: '复制成功',
              icon: 'success'
            })
          }
        })
      }
    })
  },
});

样式与动画(WXSS)

/* pages/tabs/tabs.wxss */
/* 容器样式 */
.container {
  padding: 20rpx;
}

/* 新增输入区域样式 */
.input-area {
  display: flex;
  padding: 30rpx;
  background: #fff;
}

.tag-input {
  flex: 1;
  height: 64rpx;
  padding: 0 20rpx;
  border: 2rpx solid #eee;
  border-radius: 8rpx;
  font-size: 28rpx;
  margin-right: 20rpx;
}

.add-btn {
  background: #1890ff;
  color: white;
  transition: opacity 0.2s;
  padding: auto;
}

.add-btn.active {
  opacity: 0.8;
}

.tag-container {
  display: flex;
  flex-wrap: wrap;
  padding: 30rpx;
}

/* 单个标签样式 */
.tag-item {
  position: relative;
  background: #e7f3ff;
  border-radius: 8rpx;
  padding: 12rpx 50rpx 12rpx 20rpx;
  color: #1890ff;
  transition: all 0.4s cubic-bezier(0.18, 0.89, 0.32, 1.28);
  margin: 10rpx;
  opacity: 0;
}

/* 隐藏状态动画 */
.tag-item.hidden {
  opacity: 0;
  transform: scale(0.8);
  pointer-events: none;
}

.close-btn {
  position: absolute;
  right: 8rpx;
  top: 50%;
  transform: translateY(-50%);
  width: 32rpx;
  height: 32rpx;
  line-height: 32rpx;
  text-align: center;
  border-radius: 50%;
  background: #ff4d4f;
  color: white;
  font-size: 24rpx;
  opacity: 0.8;
  transition: opacity 0.2s;
}

.close-btn.active {
  opacity: 1;
  transform: translateY(-50%) scale(0.9);
}

.reset-btn {
  margin: 20rpx auto;
  width: 70%;
  background: #1890ff;
  color: white;
}

/* 删除全部按钮样式 */
.delete-all-btn {
  margin: 20rpx auto;
  width: 40%;
  background: #ff4d4f;
  color: white;
}

.title {
  text-align: left;
  margin-top: 10px;
}

.scroll {
  height: 300px;
  width: 100%;
  margin-bottom: 15px;
}

.weui-dialog__btn_primary {
  background-color: #07c160;
  color: #fff;
}

json

{
  "usingComponents": {
    "navigation-bar": "/components/navigation-bar/navigation-bar",
    "mp-dialog": "weui-miniprogram/dialog/dialog"
  }
}

部分代码由deepseek辅助生成

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值