轻量级交互优化:基于 HTML5 Details/Summary 的供应链筛选面板实现

举报
叶一一 发表于 2025/12/20 18:14:23 2025/12/20
【摘要】 引言在现代前端开发中,用户体验始终是我们关注的核心。特别是在企业级应用中,复杂的筛选条件往往让用户感到困扰。本文将介绍如何利用 HTML5 原生的 <details> 和 <summary> 标签,结合 CSS 优化,构建一个轻量级且高效的筛选面板组件。这种方案不仅减少了 JavaScript 的依赖,还能提供流畅的动画效果,在新零售供应链系统中具有很高的实用价值。为什么选择 Details...

引言

在现代前端开发中,用户体验始终是我们关注的核心。特别是在企业级应用中,复杂的筛选条件往往让用户感到困扰。本文将介绍如何利用 HTML5 原生的 <details> <summary> 标签,结合 CSS 优化,构建一个轻量级且高效的筛选面板组件。这种方案不仅减少了 JavaScript 的依赖,还能提供流畅的动画效果,在新零售供应链系统中具有很高的实用价值。

为什么选择 Details/Summary?

传统的筛选面板通常依赖于大量的 JavaScript 来控制展开/收起状态。而 HTML5 提供的 <details> 元素天生具备这种切换能力,配合 <summary> 元素定义摘要标题,可以极大简化我们的实现逻辑。

<details>
  <summary>筛选条件</summary>
  <div class="filter-content">
    <!-- 筛选内容 -->
  </div>
</details>

这段基础代码就实现了最基本的可折叠面板功能,无需编写任何 JavaScript 代码。

核心实现思路

让我们从零开始构建一个适用于供应链系统的筛选面板组件:

1. 基础结构设计

首先我们需要确定筛选面板的基本结构。考虑到供应链系统的复杂性,我们将筛选项按照业务维度进行分类:

import React from 'react';
import './FilterPanel.css';

const FilterPanel = () => {
  return (
    <div className="supply-chain-filter">
      <details className="filter-section" open>
        <summary className="filter-summary">
          <span className="summary-title">时间范围</span>
          <span className="summary-icon">▼</span>
        </summary>
        <div className="filter-content">
          {/* 时间筛选控件 */}
        </div>
      </details>
      
      <details className="filter-section">
        <summary className="filter-summary">
          <span className="summary-title">供应商信息</span>
          <span className="summary-icon">▼</span>
        </summary>
        <div className="filter-content">
          {/* 供应商筛选控件 */}
        </div>
      </details>
      
      <details className="filter-section">
        <summary className="filter-summary">
          <span className="summary-title">商品类别</span>
          <span className="summary-icon">▼</span>
        </summary>
        <div className="filter-content">
          {/* 商品类别筛选控件 */}
        </div>
      </details>
    </div>
  );
};

export default FilterPanel;

2. CSS 样式优化

为了提升用户体验,我们需要对原生样式进行深度定制。以下是完整的 CSS 实现:

.supply-chain-filter {
  background: #ffffff;
  border-radius: 8px;
  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
  overflow: hidden;
}

.filter-section {
  border-bottom: 1px solid #ebeef5;
}

.filter-section:last-child {
  border-bottom: none;
}

.filter-summary {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 16px 20px;
  cursor: pointer;
  user-select: none;
  background-color: #f8f9fa;
  transition: all 0.3s ease;
}

.filter-summary:hover {
  background-color: #f1f3f4;
}

.summary-title {
  font-weight: 600;
  color: #303133;
  font-size: 14px;
}

.summary-icon {
  transition: transform 0.3s ease;
  color: #909399;
  font-size: 12px;
}

/* 展开状态下的图标旋转 */
.filter-section[open] .summary-icon {
  transform: rotate(180deg);
}

.filter-content {
  padding: 20px;
  background: #ffffff;
  animation: slideDown 0.3s ease forwards;
}

@keyframes slideDown {
  from {
    opacity: 0;
    max-height: 0;
  }
  to {
    opacity: 1;
    max-height: 500px;
  }
}

/* 收起动画 */
.filter-section:not([open]) .filter-content {
  animation: slideUp 0.3s ease forwards;
}

@keyframes slideUp {
  from {
    opacity: 1;
    max-height: 500px;
  }
  to {
    opacity: 0;
    max-height: 0;
  }
}

完整功能实现

现在我们来实现一个完整的供应链筛选面板,包含实际的筛选控件:

import React, { useState } from 'react';
import './SupplyChainFilter.css';

const SupplyChainFilter = ({ onFilterChange }) => {
  const [filters, setFilters] = useState({
    dateRange: { start: '', end: '' },
    supplier: '',
    category: [],
    status: ''
  });

  const handleDateChange = (type, value) => {
    setFilters(prev => ({
      ...prev,
      dateRange: {
        ...prev.dateRange,
        [type]: value
      }
    }));
  };

  const handleSupplierChange = (value) => {
    setFilters(prev => ({
      ...prev,
      supplier: value
    }));
  };

  const handleCategoryChange = (category, checked) => {
    setFilters(prev => {
      const newCategories = checked 
        ? [...prev.category, category]
        : prev.category.filter(c => c !== category);
      return {
        ...prev,
        category: newCategories
      };
    });
  };

  const handleStatusChange = (value) => {
    setFilters(prev => ({
      ...prev,
      status: value
    }));
  };

  const applyFilters = () => {
    onFilterChange(filters);
  };

  const resetFilters = () => {
    setFilters({
      dateRange: { start: '', end: '' },
      supplier: '',
      category: [],
      status: ''
    });
  };

  // 商品类别选项
  const categories = [
    '食品饮料', '日用品', '电子产品', '服装鞋帽', '家居用品', '办公用品'
  ];

  // 订单状态选项
  const statuses = [
    { value: 'all', label: '全部状态' },
    { value: 'pending', label: '待处理' },
    { value: 'processing', label: '处理中' },
    { value: 'shipped', label: '已发货' },
    { value: 'delivered', label: '已送达' },
    { value: 'cancelled', label: '已取消' }
  ];

  return (
    <div className="supply-chain-filter">
      <details className="filter-section" open>
        <summary className="filter-summary">
          <span className="summary-title">时间范围</span>
          <span className="summary-icon">▼</span>
        </summary>
        <div className="filter-content">
          <div className="date-range-controls">
            <div className="date-input-group">
              <label>开始日期</label>
              <input
                type="date"
                value={filters.dateRange.start}
                onChange={(e) => handleDateChange('start', e.target.value)}
                className="date-input"
              />
            </div>
            <div className="date-input-group">
              <label>结束日期</label>
              <input
                type="date"
                value={filters.dateRange.end}
                onChange={(e) => handleDateChange('end', e.target.value)}
                className="date-input"
              />
            </div>
          </div>
        </div>
      </details>

      <details className="filter-section">
        <summary className="filter-summary">
          <span className="summary-title">供应商信息</span>
          <span className="summary-icon">▼</span>
        </summary>
        <div className="filter-content">
          <div className="supplier-control">
            <label>供应商名称</label>
            <input
              type="text"
              placeholder="请输入供应商名称"
              value={filters.supplier}
              onChange={(e) => handleSupplierChange(e.target.value)}
              className="supplier-input"
            />
          </div>
        </div>
      </details>

      <details className="filter-section">
        <summary className="filter-summary">
          <span className="summary-title">商品类别</span>
          <span className="summary-icon">▼</span>
        </summary>
        <div className="filter-content">
          <div className="category-checkboxes">
            {categories.map(category => (
              <label key={category} className="checkbox-item">
                <input
                  type="checkbox"
                  checked={filters.category.includes(category)}
                  onChange={(e) => handleCategoryChange(category, e.target.checked)}
                  className="category-checkbox"
                />
                <span className="checkbox-label">{category}</span>
              </label>
            ))}
          </div>
        </div>
      </details>

      <details className="filter-section">
        <summary className="filter-summary">
          <span className="summary-title">订单状态</span>
          <span className="summary-icon">▼</span>
        </summary>
        <div className="filter-content">
          <div className="status-radio-group">
            {statuses.map(status => (
              <label key={status.value} className="radio-item">
                <input
                  type="radio"
                  name="orderStatus"
                  value={status.value}
                  checked={filters.status === status.value}
                  onChange={(e) => handleStatusChange(e.target.value)}
                  className="status-radio"
                />
                <span className="radio-label">{status.label}</span>
              </label>
            ))}
          </div>
        </div>
      </details>

      <div className="filter-actions">
        <button onClick={resetFilters} className="reset-btn">
          重置
        </button>
        <button onClick={applyFilters} className="apply-btn">
          应用筛选
        </button>
      </div>
    </div>
  );
};

export default SupplyChainFilter;

对应的 CSS 样式:

.supply-chain-filter {
  background: #ffffff;
  border-radius: 8px;
  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
  overflow: hidden;
  max-width: 100%;
}

.filter-section {
  border-bottom: 1px solid #ebeef5;
}

.filter-section:last-child {
  border-bottom: none;
}

.filter-summary {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 16px 20px;
  cursor: pointer;
  user-select: none;
  background-color: #f8f9fa;
  transition: all 0.3s ease;
}

.filter-summary:hover {
  background-color: #f1f3f4;
}

.summary-title {
  font-weight: 600;
  color: #303133;
  font-size: 14px;
}

.summary-icon {
  transition: transform 0.3s ease;
  color: #909399;
  font-size: 12px;
}

.filter-section[open] .summary-icon {
  transform: rotate(180deg);
}

.filter-content {
  padding: 20px;
  background: #ffffff;
  animation: slideDown 0.3s ease forwards;
}

@keyframes slideDown {
  from {
    opacity: 0;
    max-height: 0;
  }
  to {
    opacity: 1;
    max-height: 1000px;
  }
}

.filter-section:not([open]) .filter-content {
  animation: slideUp 0.3s ease forwards;
}

@keyframes slideUp {
  from {
    opacity: 1;
    max-height: 1000px;
  }
  to {
    opacity: 0;
    max-height: 0;
  }
}

/* 日期范围控件样式 */
.date-range-controls {
  display: flex;
  gap: 20px;
  flex-wrap: wrap;
}

.date-input-group {
  flex: 1;
  min-width: 200px;
}

.date-input-group label {
  display: block;
  margin-bottom: 8px;
  font-size: 13px;
  color: #606266;
}

.date-input {
  width: 100%;
  padding: 8px 12px;
  border: 1px solid #dcdfe6;
  border-radius: 4px;
  font-size: 14px;
  transition: border-color 0.3s;
}

.date-input:focus {
  outline: none;
  border-color: #409eff;
}

/* 供应商控件样式 */
.supplier-control label {
  display: block;
  margin-bottom: 8px;
  font-size: 13px;
  color: #606266;
}

.supplier-input {
  width: 100%;
  padding: 8px 12px;
  border: 1px solid #dcdfe6;
  border-radius: 4px;
  font-size: 14px;
  transition: border-color 0.3s;
}

.supplier-input:focus {
  outline: none;
  border-color: #409eff;
}

.supplier-input::placeholder {
  color: #c0c4cc;
}

/* 类别复选框样式 */
.category-checkboxes {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
  gap: 12px;
}

.checkbox-item {
  display: flex;
  align-items: center;
  cursor: pointer;
}

.category-checkbox {
  margin-right: 8px;
  cursor: pointer;
}

.checkbox-label {
  font-size: 13px;
  color: #606266;
  cursor: pointer;
}

/* 状态单选按钮样式 */
.status-radio-group {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
  gap: 12px;
}

.radio-item {
  display: flex;
  align-items: center;
  cursor: pointer;
}

.status-radio {
  margin-right: 8px;
  cursor: pointer;
}

.radio-label {
  font-size: 13px;
  color: #606266;
  cursor: pointer;
}

/* 操作按钮样式 */
.filter-actions {
  display: flex;
  justify-content: flex-end;
  padding: 20px;
  background: #f8f9fa;
  gap: 12px;
}

.reset-btn,
.apply-btn {
  padding: 10px 20px;
  border: none;
  border-radius: 4px;
  font-size: 14px;
  cursor: pointer;
  transition: all 0.3s;
}

.reset-btn {
  background: #ffffff;
  color: #606266;
  border: 1px solid #dcdfe6;
}

.reset-btn:hover {
  background: #f5f7fa;
}

.apply-btn {
  background: #409eff;
  color: #ffffff;
}

.apply-btn:hover {
  background: #66b1ff;
}

/* 响应式设计 */
@media (max-width: 768px) {
  .date-range-controls {
    flex-direction: column;
    gap: 15px;
  }
  
  .date-input-group {
    min-width: auto;
  }
  
  .category-checkboxes,
  .status-radio-group {
    grid-template-columns: 1fr 1fr;
  }
  
  .filter-actions {
    flex-direction: column;
  }
  
  .reset-btn,
  .apply-btn {
    width: 100%;
  }
}

性能优化策略

为了进一步提升用户体验,我们可以添加一些性能优化措施:

import React, { useState, useCallback, useMemo } from 'react';
import './OptimizedSupplyChainFilter.css';

const OptimizedSupplyChainFilter = React.memo(({ onFilterChange }) => {
  const [filters, setFilters] = useState({
    dateRange: { start: '', end: '' },
    supplier: '',
    category: [],
    status: ''
  });

  // 使用 useCallback 优化事件处理器
  const handleDateChange = useCallback((type, value) => {
    setFilters(prev => ({
      ...prev,
      dateRange: {
        ...prev.dateRange,
        [type]: value
      }
    }));
  }, []);

  const handleSupplierChange = useCallback((value) => {
    setFilters(prev => ({
      ...prev,
      supplier: value
    }));
  }, []);

  const handleCategoryChange = useCallback((category, checked) => {
    setFilters(prev => {
      const newCategories = checked 
        ? [...prev.category, category]
        : prev.category.filter(c => c !== category);
      return {
        ...prev,
        category: newCategories
      };
    });
  }, []);

  const handleStatusChange = useCallback((value) => {
    setFilters(prev => ({
      ...prev,
      status: value
    }));
  }, []);

  // 使用 useMemo 缓存计算结果
  const isFilterActive = useMemo(() => {
    return filters.dateRange.start || 
           filters.dateRange.end || 
           filters.supplier || 
           filters.category.length > 0 || 
           filters.status;
  }, [filters]);

  const applyFilters = useCallback(() => {
    onFilterChange(filters);
  }, [filters, onFilterChange]);

  const resetFilters = useCallback(() => {
    setFilters({
      dateRange: { start: '', end: '' },
      supplier: '',
      category: [],
      status: ''
    });
  }, []);

  // 商品类别选项
  const categories = useMemo(() => [
    '食品饮料', '日用品', '电子产品', '服装鞋帽', '家居用品', '办公用品'
  ], []);

  // 订单状态选项
  const statuses = useMemo(() => [
    { value: 'all', label: '全部状态' },
    { value: 'pending', label: '待处理' },
    { value: 'processing', label: '处理中' },
    { value: 'shipped', label: '已发货' },
    { value: 'delivered', label: '已送达' },
    { value: 'cancelled', label: '已取消' }
  ], []);

  return (
    <div className="supply-chain-filter">
      <details className="filter-section" open>
        <summary className="filter-summary">
          <span className="summary-title">时间范围</span>
          <span className="summary-icon">▼</span>
        </summary>
        <div className="filter-content">
          <div className="date-range-controls">
            <div className="date-input-group">
              <label>开始日期</label>
              <input
                type="date"
                value={filters.dateRange.start}
                onChange={(e) => handleDateChange('start', e.target.value)}
                className="date-input"
              />
            </div>
            <div className="date-input-group">
              <label>结束日期</label>
              <input
                type="date"
                value={filters.dateRange.end}
                onChange={(e) => handleDateChange('end', e.target.value)}
                className="date-input"
              />
            </div>
          </div>
        </div>
      </details>

      <details className="filter-section">
        <summary className="filter-summary">
          <span className="summary-title">供应商信息</span>
          <span className="summary-icon">▼</span>
        </summary>
        <div className="filter-content">
          <div className="supplier-control">
            <label>供应商名称</label>
            <input
              type="text"
              placeholder="请输入供应商名称"
              value={filters.supplier}
              onChange={(e) => handleSupplierChange(e.target.value)}
              className="supplier-input"
            />
          </div>
        </div>
      </details>

      <details className="filter-section">
        <summary className="filter-summary">
          <span className="summary-title">商品类别</span>
          <span className="summary-icon">▼</span>
        </summary>
        <div className="filter-content">
          <div className="category-checkboxes">
            {categories.map(category => (
              <label key={category} className="checkbox-item">
                <input
                  type="checkbox"
                  checked={filters.category.includes(category)}
                  onChange={(e) => handleCategoryChange(category, e.target.checked)}
                  className="category-checkbox"
                />
                <span className="checkbox-label">{category}</span>
              </label>
            ))}
          </div>
        </div>
      </details>

      <details className="filter-section">
        <summary className="filter-summary">
          <span className="summary-title">订单状态</span>
          <span className="summary-icon">▼</span>
        </summary>
        <div className="filter-content">
          <div className="status-radio-group">
            {statuses.map(status => (
              <label key={status.value} className="radio-item">
                <input
                  type="radio"
                  name="orderStatus"
                  value={status.value}
                  checked={filters.status === status.value}
                  onChange={(e) => handleStatusChange(e.target.value)}
                  className="status-radio"
                />
                <span className="radio-label">{status.label}</span>
              </label>
            ))}
          </div>
        </div>
      </details>

      <div className="filter-actions">
        <button 
          onClick={resetFilters} 
          className={`reset-btn ${!isFilterActive ? 'disabled' : ''}`}
          disabled={!isFilterActive}
        >
          重置
        </button>
        <button onClick={applyFilters} className="apply-btn">
          应用筛选
        </button>
      </div>
    </div>
  );
});

export default OptimizedSupplyChainFilter;

使用示例

import React, { useState } from 'react';
import SupplyChainFilter from './components/SupplyChainFilter';
import OrderList from './components/OrderList';
import './App.css';

const App = () => {
  const [filterParams, setFilterParams] = useState({});
  const [orders, setOrders] = useState([]);

  const handleFilterChange = (filters) => {
    setFilterParams(filters);
    // 这里调用 API 获取筛选后的数据
    fetchOrders(filters);
  };

  const fetchOrders = async (filters) => {
    try {
      // 模拟 API 调用
      const response = await fetch('/api/orders', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify(filters)
      });
      const data = await response.json();
      setOrders(data.orders);
    } catch (error) {
      console.error('获取订单失败:', error);
    }
  };

  return (
    <div className="app">
      <header className="app-header">
        <h1>供应链管理系统</h1>
      </header>
      <main className="app-main">
        <aside className="filter-sidebar">
          <SupplyChainFilter onFilterChange={handleFilterChange} />
        </aside>
        <section className="content-area">
          <OrderList orders={orders} />
        </section>
      </main>
    </div>
  );
};

export default App;

实际应用场景

在新零售供应链系统中,这种筛选面板特别适用于以下场景:

  • 订单管理系统 - 按时间、状态、供应商等维度筛选订单
  • 库存管理系统 - 按商品类别、仓库位置、库存状态筛选商品
  • 供应商管理系统 - 按地区、合作状态、评级筛选供应商
  • 物流跟踪系统 - 按运输状态、时间范围、目的地筛选运单

结语

通过本文的详细介绍,我们学习了如何利用 HTML5 原生的 <details> <summary> 标签构建高性能的筛选面板组件。这种方法相比传统的 JavaScript 实现有诸多优势:代码更简洁、性能更好、可访问性更强,并且天然具备良好的 SEO 特性。

在实际项目中,我们可以根据具体需求对这个基础实现进行扩展,比如添加搜索功能、多级联动筛选、本地存储用户偏好等。这种轻量级的交互方案特别适合企业级应用中的复杂表单场景,既保证了功能完整性,又提供了优秀的用户体验。

通过合理运用原生 HTML 特性和现代 CSS 动画,我们可以在不增加过多复杂度的前提下,打造出既美观又实用的前端组件。这正是现代前端开发追求的目标:用最简单的方式解决最复杂的问题。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。