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

news2025/5/19 21:52:53

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完整指南

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2379548.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

常用的Java工具库

1. Collections 首先是 java.util 包下的 Collections 类。这个类主要用于操作集合&#xff0c;我个人非常喜欢使用它。以下是一些常用功能&#xff1a; 1.1 排序 在工作中&#xff0c;经常需要对集合进行排序。让我们看看如何使用 Collections 工具实现升序和降序排列&…

R S的EMI接收机面板

图片摘自R & S官网。 根据您提供的第一张图&#xff08;设备前面板带屏幕的图像&#xff09;&#xff0c;这是 Rohde & Schwarz ESRP7 EMI Test Receiver 的正面显示界面&#xff0c;我将对屏幕上显示的参数逐项进行解读&#xff1a; &#x1f5a5;️ 屏幕参数解读 左…

[ctfshow web入门] web122

信息收集 这一题把HOME开放了&#xff0c;把#和PWD给过滤了 <?php error_reporting(0); highlight_file(__FILE__); if(isset($_POST[code])){$code$_POST[code];if(!preg_match(/\x09|\x0a|[a-z]|[0-9]|FLAG|PATH|BASH|PWD|HISTIGNORE|HISTFILESIZE|HISTFILE|HISTCMD|US…

Java虚拟机 - JVM与Java体系结构

Java虚拟机 JVM与Java体系结构为什么要学习JVMJava与JVM简介Java 语言的核心特性JVM&#xff1a;Java 生态的基石JVM的架构模型基于栈的指令集架构&#xff08;Stack-Based&#xff09;基于寄存器的指令集架构&#xff08;Register-Based&#xff09;JVM生命周期 总结 JVM与Jav…

灌区量测水自动化监测解决方案

一、方案背景 随着社会发展和人口增长&#xff0c;水资源需求不断增大。我国水资源总量虽然丰富&#xff0c;但时空分布不均&#xff0c;加之农业用水占比大且效率偏低&#xff0c;使得水资源短缺问题日益凸显。农业用水一直是我国的耗水大户&#xff0c;占全部耗水总量的60%以…

界面控件DevExpress WinForms v24.2 - 数据处理功能增强

DevExpress WinForms拥有180组件和UI库&#xff0c;能为Windows Forms平台创建具有影响力的业务解决方案。DevExpress WinForms能完美构建流畅、美观且易于使用的应用程序&#xff0c;无论是Office风格的界面&#xff0c;还是分析处理大批量的业务数据&#xff0c;它都能轻松胜…

Linux的MySQL头文件和找不到头文件问题解决

头文件 #include <iostream> #include <mysql_driver.h> #include <mysql_connection.h> #include <cppconn/statement.h> #include <cppconn/resultset.h> #include <cppconn/prepared_statement.h> #include <cppconn/exception.h&g…

wps excel将表格输出pdf时所有列在一张纸上

记录&#xff1a;wps excel将表格输出pdf时所有列在一张纸上 1&#xff0c;调整缩放比例&#xff0c;或选择将所有列打印在一页 2&#xff0c;将表格的所有铺满到这套虚线

zabbix7.2最新版本 nginx自定义监控(三) 设置触发器

安装zabbix-get服务 在zabbix-server端口安装zabbix-get服务 [rootlocalhost ~]# dnf install -y zabbix-get Last metadata expiration check: 1:55:49 ago on Wed 14 May 2025 09:24:49 AM CST. Dependencies resolved. Package Architectur…

缓存的相关内容

缓存是一种介于数据永久存储介质与数据应用之间数据临时的存储介质 实用化保存可以有效地减少低俗数据读取的次数 (例如磁盘IO), 提高系统性能 缓存不仅可以用于提高永久性存储介质的数据读取效率&#xff0c;还可以提供临时的数据存储空间 spring boot中提供了缓存技术, 方便…

[ctfshow web入门] web77

信息收集 上一题的读取flag方式不能用了&#xff0c;使用后的回显是&#xff1a;could not find driver 解题 同样的查目录方法 cvar_export(scandir("glob:///*"));die();cforeach(new DirectoryIterator("glob:///*") as $a){echo($a->__toString…

C++学习-入门到精通-【7】类的深入剖析

C学习-入门到精通-【7】类的深入剖析 类的深入剖析 C学习-入门到精通-【7】类的深入剖析一、Time类的实例研究二、组成和继承三、类的作用域和类成员的访问类作用域和块作用域圆点成员选择运算符(.)和箭头成员选择运算符(->)访问函数和工具函数 四、具有默认实参的构造函数重…

主成分分析的应用之sklearn.decomposition模块的PCA函数

主成分分析的应用之sklearn.decomposition模块的PCA函数 一、模型建立整体步骤 二、数据 2297.86 589.62 474.74 164.19 290.91 626.21 295.20 199.03 2262.19 571.69 461.25 185.90 337.83 604.78 354.66 198.96 2303.29 589.99 516.21 236.55 403.92 730.05 438.41 225.80 …

1. Go 语言环境安装

&#x1f451; 博主简介&#xff1a;高级开发工程师 &#x1f463; 出没地点&#xff1a;北京 &#x1f48a; 人生目标&#xff1a;自由 ——————————————————————————————————————————— 版权声明&#xff1a;本文为原创文章&#xf…

IP协议深度解析:互联网世界的核心基石

作为互联网通信的基础协议&#xff0c;IP&#xff08;Internet Protocol&#xff09;承载着全球99%的网络数据流量。本文将深入剖析IP协议的核心特性、工作原理及演进历程&#xff0c;通过技术原理、协议对比和实战案例分析&#xff0c;为您揭示这个数字世界"隐形交通规则…

Oracle DBMS_STATS.GATHER_DATABASE_STATS 默认行为

Oracle DBMS_STATS.GATHER_DATABASE_STATS 默认行为 DBMS_STATS.GATHER_DATABASE_STATS的默认选项究竟是’GATHER’还是’GATHER AUTO’&#xff1f;这个问题非常重要&#xff0c;因为理解默认行为直接影响统计信息收集策略。 一 官方文档确认 根据Oracle 19c官方文档&#…

C++天空之城的树 全国信息素养大赛复赛决赛 C++小学/初中组 算法创意实践挑战赛 内部集训模拟题详细解析

C++天空之城的树 全国青少年信息素养大赛 C++复赛/决赛模拟练习题 博主推荐 所有考级比赛学习相关资料合集【推荐收藏】1、C++专栏 电子学会C++一级历年真题解析

GO语言语法---switch语句

文章目录 基本语法1. 特点1.1 不需要break1.2 表达式可以是任何类型1.3 省略比较表达式1.4 多值匹配1.5 类型switch1.6 case穿透1.7 switch后直接声明变量1.7.1 基本语法1.7.2 带比较表达式1.7.3 不带比较表达式1.7.4 结合类型判断 1.8 switch后的表达式必须与case语句中的表达…

开疆智能Profient转ModbusTCP网关连接ABB机器人MODBUS TCP通讯案例

本案例是通过开疆智能Profinet转ModbusTCP网关将西门子PLC与ABB机器人进行通讯 因西门子PLC采用Profinet协议&#xff0c;而ABB机器人采用的是ModbusTCP通讯。故采取此种方案。 配置过程&#xff1a; 1.MODBUS/TCP基于以太网&#xff0c;故ABB机器人在使用时需要有616-1PCIN…

解决qt.network.ssl: QSslSocket::connectToHostEncrypted: TLS initialization failed

可以参考&#xff1a;解决qt.network.ssl: QSslSocket::connectToHostEncrypted: TLS initialization failed-CSDN博客 讲的是程序执行目录下可能缺少了&#xff1a; libssl-1_1-x64.dll 和 libcrypto-1_1-x64.dll 库文件&#xff0c;将其复制到可执行文件exe的同级目录下即可…