轻量级交互优化:基于 HTML5 Details/Summary 的供应链筛选面板实现
引言
在现代前端开发中,用户体验始终是我们关注的核心。特别是在企业级应用中,复杂的筛选条件往往让用户感到困扰。本文将介绍如何利用 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 动画,我们可以在不增加过多复杂度的前提下,打造出既美观又实用的前端组件。这正是现代前端开发追求的目标:用最简单的方式解决最复杂的问题。
- 点赞
- 收藏
- 关注作者
评论(0)