1. 项目概述

本项目是一个基于 Electron + 原生 JavaScript 构建的桌面应用程序,用于目标检测系统的前端交互界面。

核心技术组成:

  • 桌面框架: Electron v33.0.0

  • 前端语言: 原生 JavaScript (ES6+)

  • 样式方案: 原生 CSS3 (无预处理器)

  • UI 设计规范: Google Material Design 3

  • 后端通信: Fetch API / HTTP


5. 核心代码实现

5.1 页面管理器(Page Manager)

系统采用单页应用(SPA)架构,通过 PageManager 管理页面切换:

const PageManager = {
  currentPage: null,
  
  pages: {
    'image-detection': ImageDetectionPage,
    'video-detection': VideoDetectionPage,
    'system-logs': SystemLogsPage,
    'system-monitor': SystemMonitorPage,
    'llm-chat': LLMChatPage,
    'multimodal-chat': MultimodalChatPage
  },
​
  navigate(pageName) {
    // 清理当前页面
    if (this.currentPage && this.currentPage.destroy) {
      this.currentPage.destroy();
    }
​
    // 渲染新页面
    const PageClass = this.pages[pageName];
    if (PageClass) {
      this.currentPage = new PageClass();
      this.currentPage.render();
    }
  }
};

5.2 图像检测页面实现

function ImageDetectionPage() {
  this.images = [];
  this.detectedImages = [];
  this.selectedImages = new Set();
}
​
ImageDetectionPage.prototype = {
  async render() {
    const container = document.getElementById('page-container');
    container.innerHTML = `
      <div class="card">
        <div class="card-header">
          <span class="card-title">上传图像</span>
        </div>
        <div class="upload-area" id="upload-area">
          <div class="upload-icon">📤</div>
          <div class="upload-text">点击或拖拽图像到此处上传</div>
          <input type="file" id="file-input" multiple accept=".png,.jpg,.jpeg,.gif,.bmp" style="display:none">
        </div>
      </div>
      
      <div class="card">
        <div class="card-header">
          <span class="card-title">已上传图像</span>
          <div class="card-actions">
            <button class="btn btn-secondary btn-sm" οnclick="imageDetectionPage.selectAll()">全选</button>
            <button class="btn btn-secondary btn-sm" οnclick="imageDetectionPage.detectSelected()">批量检测</button>
            <button class="btn btn-danger btn-sm" οnclick="imageDetectionPage.deleteSelected()">批量删除</button>
          </div>
        </div>
        <div id="image-grid" class="media-grid"></div>
      </div>
    `;

5.3 模态框系统

const Utils = {
  previousActiveElement: null,
​
  createModal(title, content, footer = '') {
    // 保存当前焦点元素
    this.previousActiveElement = document.activeElement;
​
    const container = document.getElementById('modal-container');
    container.innerHTML = `
      <div class="modal-overlay active" role="dialog" aria-modal="true" aria-labelledby="modal-title">
        <div class="modal" tabindex="-1">
          <div class="modal-header">
            <h3 id="modal-title" class="modal-title">${title}</h3>
            <button class="modal-close" οnclick="Utils.closeModal()" aria-label="关闭">×</button>
          </div>
          <div class="modal-body">${content}</div>
          ${footer ? `<div class="modal-footer">${footer}</div>` : ''}
        </div>
      </div>
    `;

6. 样式系统详解

6.1 CSS 变量系统

所有设计 Token 通过 CSS 变量统一管理:

:root {
  /* 颜色 */
  --primary-color: #6200EE;
  --background-color: #FAFAFA;
  --surface-color: #FFFFFF;
  
  /* 文本 */
  --on-surface: #1C1B1F;
  --on-surface-medium: #49454F;
  
  /* 圆角 */
  --radius-sm: 4px;
  --radius-md: 8px;
  --radius-lg: 12px;
  --radius-xl: 16px;
  
  /* 间距 */
  --sidebar-width: 280px;
  --header-height: 72px;
}

6.2 组件样式规范

6.2.1 按钮(Button)
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 12px 28px;
  border: 2px solid transparent;
  border-radius: var(--radius-md);
  font-size: 16px;
  font-weight: 600;
  min-height: 48px;
  cursor: pointer;
  transition: var(--transition-fast);
  position: relative;
  overflow: hidden;
}
​
/* 涟漪效果 */
.btn::after {
  content: '';
  position: absolute;
  inset: 0;
  background: currentColor;
  opacity: 0;
  transition: var(--transition-fast);
}
​
.btn:hover::after {
  opacity: 0.08;
}
​
.btn:active::after {
  opacity: 0.12;
}
6.2.2 卡片(Card)
.card {
  background: var(--surface-color);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-1);
  padding: 24px;
  margin-bottom: 24px;
  transition: var(--transition-normal);
}
​
.card:hover {
  box-shadow: var(--shadow-2);
}
6.2.3 表单元素(Form)
.form-input {
  width: 100%;
  padding: 14px 16px;
  border: 2px solid var(--outline-color);
  border-radius: var(--radius-md);
  font-size: 16px;
  transition: var(--transition-fast);
}
​
.form-input:focus {
  outline: 3px solid var(--primary-color);
  outline-offset: 2px;
  border-color: var(--primary-color);
  box-shadow: 0 0 0 4px rgba(98, 0, 238, 0.1);
}

7. 性能优化策略

7.1 图片懒加载

renderImages() {
  const grid = document.getElementById('image-grid');
  grid.innerHTML = this.images.map(img => `
    <div class="media-item">
      <img src="${API_BASE}${img.url}" 
           alt="${img.filename}" 
           loading="lazy"
           οnclick="imageDetectionPage.preview('${img.filename}')">
    </div>
  `).join('');
}

7.2 防抖与节流

// 防抖(Debounce)- 用于搜索输入
function debounce(func, wait) {
  let timeout;
  return function(...args) {
    clearTimeout(timeout);
    timeout = setTimeout(() => func.apply(this, args), wait);
  };
}
​
// 节流(Throttle)- 用于滚动事件
function throttle(func, limit) {
  let inThrottle;
  return function(...args) {
    if (!inThrottle) {
      func.apply(this, args);
      inThrottle = true;
      setTimeout(() => inThrottle = false, limit);
    }
  };
}

7.3 虚拟滚动(大数据列表)

对于超过 100 项的列表,建议实现虚拟滚动:

class VirtualScroller {
  constructor(container, items, itemHeight) {
    this.container = container;
    this.items = items;
    this.itemHeight = itemHeight;
    this.visibleCount = Math.ceil(container.clientHeight / itemHeight);
    this.render();
  }
​
  render() {
    const scrollTop = this.container.scrollTop;
    const startIndex = Math.floor(scrollTop / this.itemHeight);
    const endIndex = startIndex + this.visibleCount;
    
    const visibleItems = this.items.slice(startIndex, endIndex);
    // 渲染可见项...
  }
}

8. 安全性考虑

8.1 内容安全策略(CSP)

<meta http-equiv="Content-Security-Policy"
  content="default-src 'self'; 
           script-src 'self' 'unsafe-inline'; 
           style-src 'self' 'unsafe-inline'; 
           img-src 'self' data: http://localhost:10077; 
           media-src 'self' http://localhost:10077; 
           connect-src 'self' http://localhost:10077">

8.2 XSS 防护

所有用户输入必须经过转义:

function escapeHtml(text) {
  const map = {
    '&': '&amp;',
    '<': '&lt;',
    '>': '&gt;',
    '"': '&quot;',
    "'": '&#039;'
  };
  return text.replace(/[&<>"']/g, m => map[m]);
}

9. 开发与部署

9.1 开发环境启动

# 安装依赖
npm install
​
# 启动 Electron 应用
npm run dev

9.2 生产构建

# 打包应用(需配置 electron-builder)
npm run build

9.3 调试技巧

  • 开发者工具: Ctrl+Shift+I (Windows/Linux) 或 Cmd+Option+I (Mac)

  • 重新加载: Ctrl+RCmd+R

  • 控制台日志: 所有 API 错误会自动输出到控制台


10. 最佳实践总结

10.1 代码规范

  • 使用 ES6+ 语法(箭头函数、解构、模板字符串)

  • 所有异步操作使用 async/await

  • 函数命名采用驼峰命名法(camelCase)

  • 类名采用帕斯卡命名法(PascalCase)

10.2 性能建议

  • 避免在循环中进行 DOM 操作

  • 使用事件委托处理大量元素的事件

  • 图片资源使用 WebP 格式

  • 启用浏览器缓存

10.3 可维护性

  • 每个页面独立封装为一个类

  • 工具函数统一放在 Utils 对象中

  • CSS 变量集中管理设计 Token

  • API 调用统一封装


11. 常见问题(FAQ)

Q1: 如何添加新页面?

  1. renderer.js 中创建新的页面类

  2. PageManager.pages 中注册

  3. index.html 的导航菜单中添加入口

Q2: 如何修改主题颜色?

修改 styles.css 中的 CSS 变量:

:root {
  --primary-color: #YOUR_COLOR;
}

Q3: 如何调用新的后端接口?

参考 Utils.request 方法,示例:

const result = await Utils.request('/api/your-endpoint', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ data: 'value' })
});
Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐