鸿蒙OS&UniApp制作自定义的下拉菜单组件(鸿蒙系统适配版)#三方框架 #Uniapp

发布于:2025-05-16 ⋅ 阅读:(12) ⋅ 点赞:(0)

UniApp制作自定义的下拉菜单组件(鸿蒙系统适配版)

前言

在移动应用开发中,下拉菜单是一个常见且实用的交互组件,它能在有限的屏幕空间内展示更多的选项。虽然各种UI框架都提供了下拉菜单组件,但在一些特定场景下,我们往往需要根据产品需求定制自己的下拉菜单。尤其是在鸿蒙系统逐渐普及的今天,如何让我们的组件在华为设备上有更好的表现,是值得思考的问题。

本文将分享我在实际项目中使用UniApp开发自定义下拉菜单组件的经验,包括基础实现、动画效果以及在鸿蒙系统上的特殊适配。希望能给同样面临这类需求的开发者提供一些参考。

需求分析

在开始编码前,我们先明确一下自定义下拉菜单需要满足的基本需求:

  1. 支持单选/多选模式
  2. 可自定义菜单项的样式和内容
  3. 支持搜索筛选功能
  4. 展开/收起的流畅动画
  5. 支持级联选择
  6. 良好的交互反馈
  7. 在鸿蒙系统上的适配优化

技术选型

基于上述需求,我选择的技术栈如下:

  • UniApp作为跨端开发框架
  • Vue3 + TypeScript提供响应式编程体验
  • SCSS处理样式
  • 使用CSS3实现过渡动画
  • 鸿蒙系统特有API支持

组件设计

首先,我们来设计组件的基本结构:

<template>
  <view class="custom-dropdown" :class="{'harmony-dropdown': isHarmonyOS}">
    <!-- 触发器部分 -->
    <view class="dropdown-trigger" @click="toggleDropdown">
      <text class="trigger-text">{{ triggerText }}</text>
      <view class="trigger-icon" :class="{'is-active': isOpen}">
        <text class="iconfont icon-down"></text>
      </view>
    </view>
    
    <!-- 下拉内容部分 -->
    <view 
      class="dropdown-content" 
      :class="{'is-open': isOpen}"
      :style="contentStyle"
    >
      <!-- 搜索框 -->
      <view class="search-box" v-if="showSearch">
        <input 
          type="text" 
          v-model="searchText" 
          placeholder="搜索..." 
          class="search-input"
          confirm-type="search"
          @input="handleSearch"
        />
        <text 
          class="clear-icon" 
          v-if="searchText" 
          @click.stop="clearSearch"
        >×</text>
      </view>
      
      <!-- 选项列表 -->
      <scroll-view 
        scroll-y 
        class="options-list"
        :enhanced="isHarmonyOS"
        :bounces="false"
      >
        <view 
          v-for="(item, index) in filteredOptions" 
          :key="index"
          class="option-item"
          :class="{
            'is-selected': isSelected(item),
            'harmony-item': isHarmonyOS
          }"
          @click="selectOption(item)"
        >
          <text class="option-text">{{ item[labelKey] }}</text>
          <text 
            v-if="isSelected(item)" 
            class="selected-icon iconfont icon-check"
          ></text>
        </view>
        
        <!-- 空状态 -->
        <view class="empty-tip" v-if="filteredOptions.length === 0">
          <text>无匹配结果</text>
        </view>
      </scroll-view>
      
      <!-- 操作按钮 -->
      <view class="action-btns" v-if="mode === 'multiple'">
        <view class="btn btn-clear" @click="clearSelection">清空</view>
        <view class="btn btn-confirm" @click="confirmSelection">确定</view>
      </view>
    </view>
    
    <!-- 遮罩层 -->
    <view 
      class="dropdown-mask" 
      :class="{'is-visible': isOpen}" 
      @click="closeDropdown"
    ></view>
  </view>
</template>

<script lang="ts">
import { defineComponent, ref, computed, watch, onMounted, onBeforeUnmount } from 'vue';
import { isHarmonyOS } from '@/utils/system';

export default defineComponent({
  name: 'CustomDropdown',
  props: {
    // 选项列表
    options: {
      type: Array,
      default: () => []
    },
    // 显示的键名
    labelKey: {
      type: String,
      default: 'label'
    },
    // 值的键名
    valueKey: {
      type: String,
      default: 'value'
    },
    // 选择模式:single/multiple
    mode: {
      type: String,
      default: 'single'
    },
    // 是否显示搜索框
    showSearch: {
      type: Boolean,
      default: false
    },
    // 最大高度
    maxHeight: {
      type: [String, Number],
      default: 300
    },
    // 触发器文本
    placeholder: {
      type: String,
      default: '请选择'
    },
    // 默认选中值
    modelValue: {
      type: [String, Number, Array],
      default: ''
    }
  },
  emits: ['update:modelValue', 'change', 'open', 'close'],
  setup(props, { emit }) {
    // 状态变量
    const isOpen = ref(false);
    const searchText = ref('');
    const selectedOptions = ref<any[]>([]);
    const isHarmonyOS = ref(false);
    
    // 计算下拉内容样式
    const contentStyle = computed(() => {
      const style: any = {};
      if (typeof props.maxHeight === 'number') {
        style.maxHeight = `${props.maxHeight}px`;
      } else {
        style.maxHeight = props.maxHeight;
      }
      return style;
    });
    
    // 计算过滤后的选项
    const filteredOptions = computed(() => {
      if (!searchText.value) return props.options;
      
      return props.options.filter((item: any) => {
        const label = item[props.labelKey]?.toString() || '';
        return label.toLowerCase().includes(searchText.value.toLowerCase());
      });
    });
    
    // 计算触发器显示文本
    const triggerText = computed(() => {
      if (selectedOptions.value.length === 0) {
        return props.placeholder;
      }
      
      if (props.mode === 'single') {
        return selectedOptions.value[0][props.labelKey];
      }
      
      if (selectedOptions.value.length === 1) {
        return selectedOptions.value[0][props.labelKey];
      }
      
      return `已选择${selectedOptions.value.length}项`;
    });
    
    // 初始化选中项
    const initSelection = () => {
      if (!props.modelValue) {
        selectedOptions.value = [];
        return;
      }
      
      if (props.mode === 'single') {
        const value = props.modelValue;
        const option = props.options.find((item: any) => 
          item[props.valueKey] === value
        );
        
        selectedOptions.value = option ? [option] : [];
      } else {
        const values = Array.isArray(props.modelValue) ? props.modelValue : [props.modelValue];
        selectedOptions.value = props.options.filter((item: any) => 
          values.includes(item[props.valueKey])
        );
      }
    };
    
    // 检查选项是否被选中
    const isSelected = (option: any) => {
      return selectedOptions.value.some((item: any) => 
        item[props.valueKey] === option[props.valueKey]
      );
    };
    
    // 选择选项
    const selectOption = (option: any) => {
      if (props.mode === 'single') {
        selectedOptions.value = [option];
        emitChange();
        closeDropdown();
      } else {
        const index = selectedOptions.value.findIndex((item: any) => 
          item[props.valueKey] === option[props.valueKey]
        );
        
        if (index > -1) {
          selectedOptions.value.splice(index, 1);
        } else {
          selectedOptions.value.push(option);
        }
      }
      
      // 鸿蒙系统震动反馈
      if (isHarmonyOS.value) {
        vibrateForHarmony();
      }
    };
    
    // 确认多选结果
    const confirmSelection = () => {
      emitChange();
      closeDropdown();
    };
    
    // 清空选择
    const clearSelection = () => {
      selectedOptions.value = [];
      if (props.mode === 'single') {
        emitChange();
      }
    };
    
    // 处理搜索
    const handleSearch = () => {
      // 可以添加防抖逻辑
    };
    
    // 清空搜索
    const clearSearch = () => {
      searchText.value = '';
    };
    
    // 切换下拉菜单状态
    const toggleDropdown = () => {
      isOpen.value = !isOpen.value;
      
      if (isOpen.value) {
        emit('open');
      } else {
        emit('close');
      }
    };
    
    // 关闭下拉菜单
    const closeDropdown = () => {
      if (!isOpen.value) return;
      
      isOpen.value = false;
      searchText.value = '';
      emit('close');
    };
    
    // 提交变更
    const emitChange = () => {
      let value;
      
      if (props.mode === 'single') {
        value = selectedOptions.value.length ? selectedOptions.value[0][props.valueKey] : '';
      } else {
        value = selectedOptions.value.map((item: any) => item[props.valueKey]);
      }
      
      emit('update:modelValue', value);
      emit('change', {
        value,
        options: [...selectedOptions.value]
      });
    };
    
    // 鸿蒙系统震动反馈
    const vibrateForHarmony = () => {
      // #ifdef APP-PLUS
      try {
        if (plus.os.name === 'Android' && plus.device.vendor === 'HUAWEI') {
          plus.device.vibrate(10);
        }
      } catch (e) {
        console.error('震动反馈失败', e);
      }
      // #endif
    };
    
    // 点击外部关闭
    const handleOutsideClick = (e: Event) => {
      const target = e.target as HTMLElement;
      const dropdown = document.querySelector('.custom-dropdown');
      
      if (dropdown && !dropdown.contains(target)) {
        closeDropdown();
      }
    };
    
    // 监听modelValue变化
    watch(() => props.modelValue, () => {
      initSelection();
    }, { immediate: true });
    
    // 监听options变化
    watch(() => props.options, () => {
      initSelection();
    });
    
    // 组件挂载
    onMounted(() => {
      isHarmonyOS.value = isHarmonyOS();
      initSelection();
      
      // 添加点击外部关闭事件
      document.addEventListener('click', handleOutsideClick);
    });
    
    // 组件卸载
    onBeforeUnmount(() => {
      document.removeEventListener('click', handleOutsideClick);
    });
    
    return {
      isOpen,
      searchText,
      selectedOptions,
      isHarmonyOS,
      contentStyle,
      filteredOptions,
      triggerText,
      isSelected,
      selectOption,
      confirmSelection,
      clearSelection,
      handleSearch,
      clearSearch,
      toggleDropdown,
      closeDropdown
    };
  }
});
</script>

<style lang="scss">
.custom-dropdown {
  position: relative;
  width: 100%;
  
  .dropdown-trigger {
    display: flex;
    align-items: center;
    justify-content: space-between;
    height: 80rpx;
    padding: 0 20rpx;
    background-color: #fff;
    border: 1rpx solid #ddd;
    border-radius: 8rpx;
    
    .trigger-text {
      flex: 1;
      font-size: 28rpx;
      color: #333;
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
    }
    
    .trigger-icon {
      width: 40rpx;
      text-align: center;
      transition: transform 0.3s;
      
      &.is-active {
        transform: rotate(180deg);
      }
      
      .iconfont {
        font-size: 24rpx;
        color: #666;
      }
    }
  }
  
  .dropdown-content {
    position: absolute;
    top: 90rpx;
    left: 0;
    width: 100%;
    background-color: #fff;
    border: 1rpx solid #eee;
    border-radius: 8rpx;
    box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
    z-index: 100;
    overflow: hidden;
    max-height: 0;
    opacity: 0;
    transform: translateY(-10rpx);
    transition: all 0.3s ease-out;
    
    &.is-open {
      max-height: var(--dropdown-max-height, 600rpx);
      opacity: 1;
      transform: translateY(0);
    }
    
    .search-box {
      position: relative;
      padding: 16rpx;
      border-bottom: 1rpx solid #eee;
      
      .search-input {
        width: 100%;
        height: 64rpx;
        padding: 0 60rpx 0 20rpx;
        background-color: #f5f5f5;
        border: none;
        border-radius: 32rpx;
        font-size: 26rpx;
      }
      
      .clear-icon {
        position: absolute;
        right: 36rpx;
        top: 50%;
        transform: translateY(-50%);
        width: 40rpx;
        height: 40rpx;
        line-height: 40rpx;
        text-align: center;
        font-size: 32rpx;
        color: #999;
      }
    }
    
    .options-list {
      max-height: 400rpx;
      
      .option-item {
        display: flex;
        align-items: center;
        justify-content: space-between;
        padding: 20rpx;
        border-bottom: 1rpx solid #f5f5f5;
        
        &:active {
          background-color: #f9f9f9;
        }
        
        &.is-selected {
          background-color: #f0f9ff;
          
          .option-text {
            color: #0078ff;
            font-weight: bold;
          }
          
          .selected-icon {
            color: #0078ff;
          }
        }
        
        .option-text {
          flex: 1;
          font-size: 28rpx;
          color: #333;
        }
        
        .selected-icon {
          font-size: 32rpx;
          margin-left: 10rpx;
        }
      }
      
      .empty-tip {
        padding: 40rpx 0;
        text-align: center;
        color: #999;
        font-size: 26rpx;
      }
    }
    
    .action-btns {
      display: flex;
      padding: 16rpx;
      border-top: 1rpx solid #eee;
      
      .btn {
        flex: 1;
        height: 70rpx;
        line-height: 70rpx;
        text-align: center;
        font-size: 28rpx;
        border-radius: 35rpx;
        
        &.btn-clear {
          color: #666;
          background-color: #f5f5f5;
          margin-right: 10rpx;
        }
        
        &.btn-confirm {
          color: #fff;
          background-color: #0078ff;
          margin-left: 10rpx;
        }
      }
    }
  }
  
  .dropdown-mask {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background-color: rgba(0, 0, 0, 0);
    z-index: 99;
    pointer-events: none;
    transition: background-color 0.3s;
    
    &.is-visible {
      background-color: rgba(0, 0, 0, 0.4);
      pointer-events: auto;
    }
  }
}

/* 鸿蒙系统特有样式 */
.harmony-dropdown {
  .dropdown-trigger {
    border-radius: 16rpx;
    border: none;
    background-color: #f5f7fa;
    box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
    
    .trigger-text {
      font-family: 'HarmonyOS Sans', sans-serif;
    }
  }
  
  .dropdown-content {
    border-radius: 20rpx;
    border: none;
    box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.12);
    
    .search-box {
      padding: 24rpx 20rpx 16rpx;
      
      .search-input {
        background-color: #f5f7fa;
        border-radius: 20rpx;
        height: 72rpx;
      }
    }
    
    .options-list {
      .option-item {
        &.harmony-item {
          padding: 24rpx 20rpx;
          
          &.is-selected {
            background: linear-gradient(to right, #f0f7ff, #f5faff);
            
            .option-text {
              background: linear-gradient(to right, #0078ff, #0092ff);
              -webkit-background-clip: text;
              color: transparent;
            }
          }
          
          &:active {
            background-color: #f7f9fc;
          }
        }
      }
    }
    
    .action-btns {
      padding: 20rpx;
      
      .btn {
        border-radius: 20rpx;
        height: 80rpx;
        line-height: 80rpx;
        font-family: 'HarmonyOS Sans', sans-serif;
        
        &.btn-clear {
          background-color: #f5f7fa;
        }
        
        &.btn-confirm {
          background: linear-gradient(to right, #0078ff, #0092ff);
          box-shadow: 0 4rpx 16rpx rgba(0, 120, 255, 0.3);
        }
      }
    }
  }
}
</style>

鸿蒙系统适配关键点

在为鸿蒙系统适配我们的下拉菜单组件时,需要特别注意以下几点:

1. 检测鸿蒙系统

首先,我们需要一个工具函数来检测当前设备是否运行鸿蒙系统:

// utils/system.ts

/**
 * 检测当前设备是否为鸿蒙系统
 */
export function isHarmonyOS(): boolean {
  // #ifdef APP-PLUS
  const systemInfo = uni.getSystemInfoSync();
  const systemName = systemInfo.osName || '';
  const systemVersion = systemInfo.osVersion || '';
  
  // 鸿蒙系统识别
  return systemName.toLowerCase().includes('harmony') || 
         (systemName === 'android' && systemVersion.includes('harmony'));
  // #endif
  
  return false;
}

2. UI风格适配

鸿蒙系统的设计语言强调简洁、轻盈、自然,需要适配以下UI细节:

  1. 圆角设计:鸿蒙系统偏好较大的圆角,我们在组件中使用了20rpx的圆角值
  2. 渐变色:按钮和激活态使用渐变色提升视觉效果
  3. 阴影效果:适当的阴影增强层次感,但要保持轻盈质感
  4. 字体适配:使用鸿蒙系统的HarmonyOS Sans字体
  5. 间距调整:鸿蒙UI通常有更宽松的内边距

3. 交互体验优化

鸿蒙系统注重流畅的交互体验:

  1. 震动反馈:选择选项时添加轻微震动
  2. 滚动优化:使用enhanced模式增强滚动性能
  3. 过渡动画:确保展开/收起有流畅的过渡效果
// 鸿蒙系统震动反馈
const vibrateForHarmony = () => {
  // #ifdef APP-PLUS
  try {
    if (plus.os.name === 'Android' && plus.device.vendor === 'HUAWEI') {
      plus.device.vibrate(10); // 非常轻微的震动,提供触觉反馈
    }
  } catch (e) {
    console.error('震动反馈失败', e);
  }
  // #endif
};

实际应用案例

案例一:筛选条件下拉菜单

在一个电商App的商品列表页中,我们使用了自定义下拉菜单组件来实现筛选功能。用户可以通过下拉菜单选择价格区间、品牌、尺寸等筛选条件。

<template>
  <view class="filter-bar">
    <custom-dropdown
      v-model="selectedPrice"
      :options="priceOptions"
      placeholder="价格"
      label-key="label"
      value-key="value"
      mode="single"
      @change="applyFilter"
    ></custom-dropdown>
    
    <custom-dropdown
      v-model="selectedBrands"
      :options="brandOptions"
      placeholder="品牌"
      label-key="name"
      value-key="id"
      mode="multiple"
      show-search
      @change="applyFilter"
    ></custom-dropdown>
    
    <custom-dropdown
      v-model="selectedSort"
      :options="sortOptions"
      placeholder="排序"
      @change="applyFilter"
    ></custom-dropdown>
  </view>
</template>

<script>
import CustomDropdown from '@/components/CustomDropdown.vue';

export default {
  components: {
    CustomDropdown
  },
  data() {
    return {
      selectedPrice: '',
      selectedBrands: [],
      selectedSort: 'default',
      priceOptions: [
        { label: '全部', value: '' },
        { label: '0-100元', value: '0-100' },
        { label: '100-300元', value: '100-300' },
        { label: '300-500元', value: '300-500' },
        { label: '500元以上', value: '500-' }
      ],
      brandOptions: [
        { name: '华为', id: 'huawei' },
        { name: '小米', id: 'xiaomi' },
        { name: '苹果', id: 'apple' },
        { name: '三星', id: 'samsung' },
        { name: 'OPPO', id: 'oppo' },
        { name: 'vivo', id: 'vivo' }
      ],
      sortOptions: [
        { label: '默认排序', value: 'default' },
        { label: '价格从低到高', value: 'price-asc' },
        { label: '价格从高到低', value: 'price-desc' },
        { label: '销量优先', value: 'sales-desc' },
        { label: '评分优先', value: 'rating-desc' }
      ]
    };
  },
  methods: {
    applyFilter() {
      // 应用筛选条件
      this.$emit('filter-change', {
        price: this.selectedPrice,
        brands: this.selectedBrands,
        sort: this.selectedSort
      });
    }
  }
};
</script>

案例二:级联选择器

我们还使用自定义下拉菜单组件实现了地址选择的级联选择器,用户可以依次选择省、市、区。

<template>
  <view class="address-selector">
    <custom-dropdown
      v-model="selectedProvince"
      :options="provinces"
      placeholder="选择省份"
      @change="onProvinceChange"
    ></custom-dropdown>
    
    <custom-dropdown
      v-model="selectedCity"
      :options="cities"
      placeholder="选择城市"
      :disabled="!selectedProvince"
      @change="onCityChange"
    ></custom-dropdown>
    
    <custom-dropdown
      v-model="selectedDistrict"
      :options="districts"
      placeholder="选择区县"
      :disabled="!selectedCity"
      @change="onDistrictChange"
    ></custom-dropdown>
  </view>
</template>

<script>
import { defineComponent, ref, watch } from 'vue';
import CustomDropdown from '@/components/CustomDropdown.vue';
import { fetchProvinces, fetchCities, fetchDistricts } from '@/api/address';

export default defineComponent({
  components: {
    CustomDropdown
  },
  emits: ['change'],
  setup(props, { emit }) {
    const selectedProvince = ref('');
    const selectedCity = ref('');
    const selectedDistrict = ref('');
    
    const provinces = ref([]);
    const cities = ref([]);
    const districts = ref([]);
    
    // 加载省份数据
    const loadProvinces = async () => {
      try {
        provinces.value = await fetchProvinces();
      } catch (error) {
        console.error('加载省份失败', error);
      }
    };
    
    // 加载城市数据
    const loadCities = async (provinceId) => {
      if (!provinceId) {
        cities.value = [];
        return;
      }
      
      try {
        cities.value = await fetchCities(provinceId);
      } catch (error) {
        console.error('加载城市失败', error);
      }
    };
    
    // 加载区县数据
    const loadDistricts = async (cityId) => {
      if (!cityId) {
        districts.value = [];
        return;
      }
      
      try {
        districts.value = await fetchDistricts(cityId);
      } catch (error) {
        console.error('加载区县失败', error);
      }
    };
    
    // 省份变化
    const onProvinceChange = () => {
      selectedCity.value = '';
      selectedDistrict.value = '';
      loadCities(selectedProvince.value);
      emitChange();
    };
    
    // 城市变化
    const onCityChange = () => {
      selectedDistrict.value = '';
      loadDistricts(selectedCity.value);
      emitChange();
    };
    
    // 区县变化
    const onDistrictChange = () => {
      emitChange();
    };
    
    // 发送变化事件
    const emitChange = () => {
      emit('change', {
        province: selectedProvince.value,
        city: selectedCity.value,
        district: selectedDistrict.value
      });
    };
    
    // 初始化
    onMounted(() => {
      loadProvinces();
    });
    
    return {
      selectedProvince,
      selectedCity,
      selectedDistrict,
      provinces,
      cities,
      districts,
      onProvinceChange,
      onCityChange,
      onDistrictChange
    };
  }
});
</script>

常见问题与解决方案

在开发和使用这个组件的过程中,我遇到了一些常见问题,分享解决方案:

1. 下拉菜单被裁剪问题

问题:当下拉菜单位于页面底部时,展开的内容可能会被裁剪。

解决方案:计算剩余空间,动态调整下拉方向:

const adjustDropdownPosition = () => {
  const triggerEl = triggerRef.value;
  const contentEl = contentRef.value;
  
  if (!triggerEl || !contentEl) return;
  
  // 获取触发器位置信息
  const rect = triggerEl.getBoundingClientRect();
  // 视窗高度
  const viewHeight = window.innerHeight;
  // 触发器底部到视窗底部的距离
  const spaceBelow = viewHeight - rect.bottom;
  // 内容高度
  const contentHeight = contentEl.offsetHeight;
  
  // 如果下方空间不足,向上展开
  if (spaceBelow < contentHeight && rect.top > contentHeight) {
    dropdownDirection.value = 'up';
  } else {
    dropdownDirection.value = 'down';
  }
};

2. 多个下拉菜单同时打开问题

问题:当页面中有多个下拉菜单时,打开一个菜单,其他已打开的菜单应该自动关闭。

解决方案:使用全局事件总线管理下拉菜单的打开状态:

// 全局事件总线
const emitter = mitt();

// 打开下拉菜单
const openDropdown = () => {
  // 通知其他下拉菜单关闭
  emitter.emit('dropdown-open', dropdownId.value);
  
  isOpen.value = true;
  emit('open');
};

onMounted(() => {
  // 监听其他下拉菜单打开事件
  emitter.on('dropdown-open', (id) => {
    if (id !== dropdownId.value && isOpen.value) {
      isOpen.value = false;
      emit('close');
    }
  });
});

onBeforeUnmount(() => {
  emitter.off('dropdown-open');
});

3. 在鸿蒙系统上的滚动卡顿问题

问题:在某些华为设备上,下拉菜单内容滚动不够流畅。

解决方案:开启硬件加速和使用Native View:

<scroll-view 
  scroll-y 
  class="options-list"
  :enhanced="isHarmonyOS"
  :show-scrollbar="false"
  :fast-deceleration="isHarmonyOS"
  :bounces="false"
>

同时,对滚动容器添加硬件加速样式:

.options-list {
  transform: translateZ(0);
  -webkit-overflow-scrolling: touch;
  will-change: scroll-position;
}

总结

通过本文,我们详细介绍了如何使用UniApp开发一个自定义下拉菜单组件,并特别关注了在鸿蒙系统上的适配优化。从组件的基本结构设计,到交互细节的处理,再到在实际应用中的案例展示,希望能给大家提供一些思路。

随着鸿蒙系统的普及,做好相关适配工作将越来越重要。在下拉菜单这样的基础交互组件上,通过一些细节的优化,可以大大提升用户体验,尤其是在华为设备上。

最后,欢迎大家基于这个组件进行二次开发,添加更多功能或者根据自己的业务需求进行定制。如有任何问题或改进建议,也欢迎交流讨论。

参考资源

  1. UniApp官方文档
  2. HarmonyOS设计指南
  3. Vue3官方文档
  4. CSS Animation完整指南

网站公告

今日签到

点亮在社区的每一天
去签到