Electron for鸿蒙pc项目实战之下拉菜单组件

项目介绍

这是一个基于Electron开发的下拉菜单组件示例,展示了如何在Electron应用中实现功能丰富、交互友好的下拉菜单。该组件适用于桌面应用的导航菜单、上下文菜单、设置选项等多种场景,为用户提供便捷的选项选择方式。

功能特点

  • 多种菜单变体:支持默认菜单、带图标菜单、多选菜单、级联菜单等多种类型
  • 丰富的交互效果:包含悬停高亮、点击选中、展开/折叠动画等
  • 键盘导航支持:支持通过方向键导航菜单选项,回车确认选择
  • 自定义样式:可配置菜单宽度、背景色、字体颜色、边框等样式
  • 事件回调:提供选择、取消、打开、关闭等多种事件回调
  • 级联子菜单:支持多级子菜单的嵌套展示和交互
  • 响应式设计:适配不同屏幕尺寸和窗口大小

技术实现

主进程实现 (main.js)

  • 使用Electron的appBrowserWindow模块创建应用窗口
  • 配置窗口大小为800x600像素
  • 设置安全策略:禁用Node.js集成,启用上下文隔离
  • 预加载脚本配置,确保渲染进程安全访问Electron API

预加载脚本 (preload.js)

  • 使用contextBridge模块安全地暴露Electron API到渲染进程
  • 目前组件主要在渲染进程实现,暴露空的electronAPI对象

渲染进程实现 (renderer.js)

  • 实现DropdownManager类,统一管理所有下拉菜单
  • 支持动态创建和销毁菜单
  • 实现菜单的显示、隐藏、展开、折叠逻辑
  • 事件处理:点击触发、悬停效果、键盘导航、选项选择等
  • 级联菜单处理:子菜单的展开和关闭逻辑
  • 多选菜单支持:选项的选中状态管理

界面设计 (index.html, style.css)

  • 下拉菜单基础样式:边框、阴影、圆角等
  • 菜单选项样式:正常状态、悬停状态、选中状态
  • 图标集成:支持为菜单项添加图标
  • 动画效果:展开/折叠的平滑过渡动画
  • 级联菜单指示器:右侧箭头指示子菜单的存在
  • 响应式布局:在不同屏幕尺寸下的自适应表现

代码结构

复制代码
84-dropdown-menu/
├── main.js          # Electron主进程
├── preload.js       # 预加载脚本
├── index.html       # 主界面
├── style.css        # 样式文件
├── renderer.js      # 渲染进程脚本
└── package.json     # 项目配置

核心代码说明

javascript 复制代码
class DropdownManager {
  constructor() {
    this.activeDropdowns = [];
    this.initEvents();
  }

  // 初始化全局事件
  initEvents() {
    // 点击文档其他地方关闭下拉菜单
    document.addEventListener('click', (e) => {
      this.activeDropdowns.forEach(dropdown => {
        const trigger = document.querySelector(`[data-dropdown="${dropdown.id}"]`);
        if (trigger && !trigger.contains(e.target) && !dropdown.contains(e.target)) {
          this.close(dropdown);
        }
      });
    });

    // 键盘导航
    document.addEventListener('keydown', (e) => {
      if (this.activeDropdowns.length === 0) return;
      
      const activeDropdown = this.activeDropdowns[this.activeDropdowns.length - 1];
      const items = activeDropdown.querySelectorAll('.dropdown-item:not(.dropdown-item-disabled)');
      const activeItem = activeDropdown.querySelector('.dropdown-item-active');
      const activeIndex = Array.from(items).indexOf(activeItem);
      
      switch (e.key) {
        case 'ArrowDown':
          e.preventDefault();
          const nextIndex = activeIndex < items.length - 1 ? activeIndex + 1 : 0;
          this.activateItem(items[nextIndex], activeDropdown);
          break;
        case 'ArrowUp':
          e.preventDefault();
          const prevIndex = activeIndex > 0 ? activeIndex - 1 : items.length - 1;
          this.activateItem(items[prevIndex], activeDropdown);
          break;
        case 'Enter':
          if (activeItem) {
            activeItem.click();
          }
          break;
        case 'Escape':
          this.close(activeDropdown);
          break;
      }
    });
  }

  // 激活菜单项
  activateItem(item, dropdown) {
    // 移除之前激活的项
    dropdown.querySelectorAll('.dropdown-item-active').forEach(el => {
      el.classList.remove('dropdown-item-active');
    });
    
    // 激活当前项
    item.classList.add('dropdown-item-active');
    
    // 滚动到可视区域
    item.scrollIntoView({ block: 'nearest' });
  }

  // 切换下拉菜单
  toggle(dropdownId) {
    const dropdown = document.getElementById(dropdownId);
    if (!dropdown) return;
    
    // 检查是否已经激活
    const isActive = this.activeDropdowns.includes(dropdown);
    
    if (isActive) {
      this.close(dropdown);
    } else {
      this.open(dropdown);
    }
  }

  // 打开下拉菜单
  open(dropdown) {
    // 关闭其他所有下拉菜单
    this.activeDropdowns.forEach(d => this.close(d));
    
    // 添加到活动下拉菜单列表
    this.activeDropdowns.push(dropdown);
    
    // 显示下拉菜单
    dropdown.style.display = 'block';
    
    // 触发动画
    setTimeout(() => {
      dropdown.classList.add('dropdown-open');
    }, 10);
  }

  // 关闭下拉菜单
  close(dropdown) {
    if (!dropdown) return;
    
    // 移除动画
    dropdown.classList.remove('dropdown-open');
    
    // 隐藏下拉菜单
    setTimeout(() => {
      dropdown.style.display = 'none';
      
      // 从活动下拉菜单列表中移除
      const index = this.activeDropdowns.indexOf(dropdown);
      if (index > -1) {
        this.activeDropdowns.splice(index, 1);
      }
      
      // 清除激活状态
      dropdown.querySelectorAll('.dropdown-item-active').forEach(el => {
        el.classList.remove('dropdown-item-active');
      });
    }, 300);
  }
}

创建下拉菜单

javascript 复制代码
function createDropdown(options) {
  const defaultOptions = {
    id: `dropdown-${Date.now()}`,
    triggerId: null,
    items: [],
    position: 'bottom', // top, bottom, left, right
    width: 'auto',
    multiSelect: false
  };

  const settings = { ...defaultOptions, ...options };
  
  // 创建下拉菜单容器
  const dropdown = document.createElement('div');
  dropdown.id = settings.id;
  dropdown.className = `dropdown dropdown-${settings.position}`;
  dropdown.style.width = settings.width;
  dropdown.style.display = 'none';
  
  // 生成菜单项
  const menu = document.createElement('ul');
  menu.className = 'dropdown-menu';
  
  settings.items.forEach(item => {
    const li = document.createElement('li');
    li.className = 'dropdown-item';
    
    // 添加禁用状态
    if (item.disabled) {
      li.classList.add('dropdown-item-disabled');
    }
    
    // 添加图标
    if (item.icon) {
      const icon = document.createElement('span');
      icon.className = 'dropdown-item-icon';
      icon.innerHTML = item.icon;
      li.appendChild(icon);
    }
    
    // 添加文本
    const text = document.createElement('span');
    text.className = 'dropdown-item-text';
    text.textContent = item.text;
    li.appendChild(text);
    
    // 添加多选框
    if (settings.multiSelect) {
      const checkbox = document.createElement('span');
      checkbox.className = 'dropdown-item-checkbox';
      if (item.selected) {
        checkbox.classList.add('dropdown-item-checked');
      }
      li.appendChild(checkbox);
    }
    
    // 添加子菜单指示器
    if (item.submenu && item.submenu.length > 0) {
      const arrow = document.createElement('span');
      arrow.className = 'dropdown-item-arrow';
      arrow.innerHTML = '>>';
      li.appendChild(arrow);
      
      // 创建子菜单
      const submenu = createSubmenu(item.submenu);
      li.appendChild(submenu);
    }
    
    // 添加点击事件
    if (!item.disabled) {
      li.addEventListener('click', () => {
        // 处理多选
        if (settings.multiSelect) {
          const checkbox = li.querySelector('.dropdown-item-checkbox');
          checkbox.classList.toggle('dropdown-item-checked');
        }
        
        // 执行回调
        if (item.callback) {
          item.callback();
        }
        
        // 非多选菜单点击后关闭
        if (!settings.multiSelect && !item.submenu) {
          dropdownManager.close(dropdown);
        }
      });
      
      // 添加悬停事件
      li.addEventListener('mouseenter', () => {
        // 关闭其他子菜单
        li.querySelectorAll('.dropdown-submenu').forEach(sub => {
          dropdownManager.close(sub);
        });
        
        // 打开当前子菜单
        if (item.submenu && item.submenu.length > 0) {
          const submenu = li.querySelector('.dropdown-submenu');
          dropdownManager.open(submenu);
        }
      });
    }
    
    menu.appendChild(li);
  });
  
  dropdown.appendChild(menu);
  
  // 添加到文档
  document.body.appendChild(dropdown);
  
  // 绑定触发器
  if (settings.triggerId) {
    const trigger = document.getElementById(settings.triggerId);
    if (trigger) {
      trigger.dataset.dropdown = settings.id;
      trigger.addEventListener('click', (e) => {
        e.stopPropagation();
        dropdownManager.toggle(settings.id);
      });
    }
  }
  
  return dropdown;
}

// 创建子菜单
function createSubmenu(items) {
  const submenu = document.createElement('div');
  submenu.className = 'dropdown dropdown-submenu';
  submenu.style.display = 'none';
  
  const menu = document.createElement('ul');
  menu.className = 'dropdown-menu';
  
  items.forEach(item => {
    // 与主菜单项创建逻辑类似
    const li = document.createElement('li');
    li.className = 'dropdown-item';
    
    // 这里省略部分代码,与主菜单类似
    // ...
  });
  
  submenu.appendChild(menu);
  return submenu;
}

如何运行

  1. 安装依赖
bash 复制代码
npm install
  1. 启动应用
bash 复制代码
npm start

扩展建议

  • 添加右键菜单功能
  • 实现菜单搜索功能
  • 支持自定义快捷键
  • 添加动画变体选择
  • 实现深色/浅色主题切换

注意事项

  • 菜单层级不宜过深,影响用户体验
  • 确保菜单项有足够的点击区域
  • 菜单项文本不宜过长,需要考虑换行或截断
  • 在实际应用中,考虑性能优化,避免过多DOM操作

鸿蒙PC适配改造指南

1. 环境准备

  • 系统要求:Windows 10/11、8GB RAM以上、20GB可用空间

  • 工具安装

    DevEco Studio 5.0+(安装鸿蒙SDK API 20+)

  • Node.js 18.x+

2. 获取Electron鸿蒙编译产物

  1. 登录Electron 鸿蒙官方仓库

  2. 下载Electron 34+版本的Release包(.zip格式)

  3. 解压到项目目录,确认electron/libs/arm64-v8a/下包含核心.so库

3. 部署应用代码

将Electron应用代码按以下目录结构放置:

plaintext 复制代码
web_engine/src/main/resources/resfile/resources/app/
├── main.js
├── package.json
└── src/
    ├── index.html
    ├── preload.js
    ├── renderer.js
    └── style.css

4. 配置与运行

  1. 打开项目:在DevEco Studio中打开ohos_hap目录

  2. 配置签名

    进入File → Project Structure → Signing Configs

  3. 自动生成调试签名或导入已有签名

  4. 连接设备

    启用鸿蒙设备开发者模式和USB调试

  5. 通过USB Type-C连接电脑

  6. 编译运行:点击Run按钮或按Shift+F10

5. 验证检查项

  • ✅ 应用窗口正常显示

  • ✅ 窗口大小可调整,响应式布局生效

  • ✅ 控制台无"SysCap不匹配"或"找不到.so文件"错误

  • ✅ 动画效果正常播放

跨平台兼容性

平台 适配策略 特殊处理
Windows 标准Electron运行 无特殊配置
macOS 标准Electron运行 保留dock图标激活逻辑
Linux 标准Electron运行 确保系统依赖库完整
鸿蒙PC 通过Electron鸿蒙适配层 禁用硬件加速,使用特定目录结构

鸿蒙开发调试技巧

1. 日志查看

在DevEco Studio的Log面板中过滤"Electron"关键词,查看应用运行日志和错误信息。

2. 常见问题解决

  • "SysCap不匹配"错误:检查module.json5中的reqSysCapabilities,只保留必要系统能力

  • "找不到.so文件"错误:确认arm64-v8a目录下四个核心库文件完整

  • 窗口不显示:在main.js中添加app.disableHardwareAcceleration()

  • 动画卡顿:简化CSS动画效果,减少重绘频率

相关推荐
国服第二切图仔1 小时前
electron for 鸿蒙PC项目实战之loading-animation组件
javascript·electron·鸿蒙pc
软件技术NINI1 小时前
html css js网页制作成品——敖瑞鹏html+css+js 4页附源码
javascript·css·html
程序员小寒1 小时前
Vue.js 为什么要推出 Vapor Mode?
前端·javascript·vue.js
汉堡黄•᷄ࡇ•᷅1 小时前
鸿蒙开发:案例集合List:多级列表(商品分类)
harmonyos·鸿蒙·鸿蒙系统
白菜__1 小时前
去哪儿小程序逆向分析(酒店)
前端·javascript·爬虫·网络协议·小程序·node.js
困惑阿三2 小时前
深入理解 JavaScript 中的(Promise.race)
开发语言·前端·javascript·ecmascript·reactjs
我命由我123452 小时前
微信小程序 bind:tap 与 bindtap 的区别
开发语言·前端·javascript·微信小程序·小程序·前端框架·js
克喵的水银蛇2 小时前
Flutter 布局实战:掌握 Row/Column/Flex 弹性布局
前端·javascript·flutter
哆啦A梦15882 小时前
60 订单页选择收货地址
前端·javascript·vue.js·node.js