欢迎加入开源鸿蒙PC社区:
atomgit仓库地址: https://atomgit.com/ai_lingshi/jianshenqicai




一、项目概述
1.1 项目背景
健身房的器材管理是健身行业运营的核心环节。传统的器材管理方式依赖纸质记录或简单的电子表格,存在以下问题:
| 问题 | 影响 |
|---|---|
| 信息不集中 | 需要在多个地方查找器材信息 |
| 维护提醒不及时 | 容易错过器材维护时间 |
| 使用统计困难 | 难以分析器材使用频率 |
| 状态更新滞后 | 无法实时了解器材可用性 |
本项目旨在通过数字化手段,实现健身房器材的智能化管理,提升运营效率。
1.2 功能定位
核心功能:
- 器材信息集中管理
- 实时状态追踪
- 智能维护提醒
- 使用数据分析
用户群体:
- 健身房管理员
- 器材维护人员
- 健身教练
- 场馆运营者
1.3 技术架构
┌─────────────────────────────────────────────────────────────┐
│ 视图层 (View) │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ HTML │ │ CSS │ │ 组件卡片 │ │
│ │ (结构) │ │ (样式) │ │ (渲染) │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
├─────────────────────────────────────────────────────────────┤
│ 控制层 (Controller) │
│ ┌─────────────────────────────────────────────────┐ │
│ │ EquipmentController │ │
│ │ - 事件监听与处理 │ │
│ │ - 业务逻辑编排 │ │
│ │ - 视图更新协调 │ │
│ └─────────────────────────────────────────────────┘ │
├─────────────────────────────────────────────────────────────┤
│ 模型层 (Model) │
│ ┌─────────────────────────────────────────────────┐ │
│ │ EquipmentModel │ │
│ │ - 数据结构定义 │ │
│ │ - CRUD 操作实现 │ │
│ │ - 业务规则校验 │ │
│ │ - 统计分析计算 │ │
│ └─────────────────────────────────────────────────┘ │
├─────────────────────────────────────────────────────────────┤
│ 存储层 (Storage) │
│ localStorage 本地持久化 │
└─────────────────────────────────────────────────────────────┘
二、数据模型设计
2.1 器材数据结构
javascript
{
id: Number, // 唯一标识符
name: String, // 器材名称
category: String, // 器材分类
status: String, // 当前状态
purchaseDate: String, // 购买日期
lastMaintenance: String, // 上次维护日期
nextMaintenance: String, // 下次维护日期
usageCount: Number, // 使用次数
notes: String // 备注信息
}
2.2 分类枚举
javascript
const CATEGORIES = {
cardio: 'cardio', // 有氧器材
strength: 'strength', // 力量器材
freeweight: 'freeweight', // 自由重量
stretch: 'stretch', // 拉伸器材
other: 'other' // 其他器材
};
器材分类详解:
| 分类 | 代码 | 典型器材 | 特点 |
|---|---|---|---|
| 有氧器材 | cardio | 跑步机、椭圆机、划船机、动感单车 | 高频使用,需定期保养 |
| 力量器材 | strength | 卧推架、深蹲架、龙门架、腿举机 | 承重要求高,安全第一 |
| 自由重量 | freeweight | 哑铃组、杠铃组、壶铃 | 使用灵活,损耗较快 |
| 拉伸器材 | stretch | 瑜伽垫、泡沫轴、拉伸带 | 低损耗,使用频率高 |
| 其他器材 | other | 跳绳、健身球、战绳 | 辅助训练,易于管理 |
2.3 状态枚举
javascript
const STATUS = {
available: 'available', // 可用
inuse: 'inuse', // 使用中
maintenance: 'maintenance' // 维护中
};
状态流转规则:
可用 ──────────→ 使用中
↑ │
│ ↓
└──────────←─── 使用完成
可用 ──────────→ 维护中
↑ │
│ ↓
└──────────←─── 维护完成
三、核心功能实现
3.1 数据模型类实现
javascript
class EquipmentModel {
constructor() {
this.equipment = [];
this.loadData();
}
// 加载数据
loadData() {
const saved = localStorage.getItem('fitnessEquipment');
if (saved) {
try {
this.equipment = JSON.parse(saved);
} catch (e) {
this.equipment = this.getDefaultData();
}
} else {
this.equipment = this.getDefaultData();
this.saveData();
}
}
// 保存数据
saveData() {
localStorage.setItem('fitnessEquipment',
JSON.stringify(this.equipment));
}
}
3.2 CRUD 操作实现
javascript
// 添加器材
add(equipment) {
const newId = Math.max(0, ...this.equipment.map(e => e.id)) + 1;
const newEquipment = {
id: newId,
...equipment,
usageCount: equipment.usageCount || 0
};
this.equipment.push(newEquipment);
this.saveData();
return newEquipment;
}
// 更新器材
update(id, data) {
const index = this.equipment.findIndex(e => e.id === id);
if (index !== -1) {
this.equipment[index] = {
...this.equipment[index],
...data
};
this.saveData();
return this.equipment[index];
}
return null;
}
// 删除器材
delete(id) {
const index = this.equipment.findIndex(e => e.id === id);
if (index !== -1) {
const deleted = this.equipment.splice(index, 1)[0];
this.saveData();
return deleted;
}
return null;
}
3.3 搜索与筛选
javascript
// 搜索器材
search(keyword) {
const lowerKeyword = keyword.toLowerCase();
return this.equipment.filter(e =>
e.name.toLowerCase().includes(lowerKeyword) ||
e.category.toLowerCase().includes(lowerKeyword) ||
(e.notes && e.notes.toLowerCase().includes(lowerKeyword))
);
}
// 按分类筛选
filterByCategory(category) {
if (category === 'all') return this.getAll();
return this.equipment.filter(e => e.category === category);
}
// 按状态筛选
filterByStatus(status) {
if (status === 'all') return this.getAll();
return this.equipment.filter(e => e.status === status);
}
搜索优化策略:
| 策略 | 说明 | 适用场景 |
|---|---|---|
| 模糊匹配 | 支持部分关键词搜索 | 用户输入不完整时 |
| 大小写不敏感 | 忽略大小写差异 | 提高搜索灵活性 |
| 多字段搜索 | 同时搜索名称、分类、备注 | 增加搜索覆盖面 |
四、统计分析功能
4.1 统计数据计算
javascript
// 获取统计数据
getStats() {
const total = this.equipment.length;
const available = this.equipment
.filter(e => e.status === 'available').length;
const inuse = this.equipment
.filter(e => e.status === 'inuse').length;
const maintenance = this.equipment
.filter(e => e.status === 'maintenance').length;
return { total, available, inuse, maintenance };
}
4.2 使用频率排行
javascript
// 获取使用频率TOP5
getTopUsage() {
return [...this.equipment]
.sort((a, b) => b.usageCount - a.usageCount)
.slice(0, 5);
}
排序算法分析:
javascript
// 时间复杂度: O(n log n)
// 空间复杂度: O(n)
const sorted = [...this.equipment]
.sort((a, b) => b.usageCount - a.usageCount)
.slice(0, 5);
// 或者使用更高效的方式(只需找到前5个)
const sorted = this.equipment
.sort((a, b) => b.usageCount - a.usageCount)
.slice(0, 5);
4.3 维护提醒系统
javascript
// 获取维护提醒
getMaintenanceAlerts() {
const today = new Date();
today.setHours(0, 0, 0, 0);
return this.equipment
.filter(e => e.nextMaintenance)
.map(e => {
const nextDate = new Date(e.nextMaintenance);
nextDate.setHours(0, 0, 0, 0);
const daysUntil = Math.ceil(
(nextDate - today) / (1000 * 60 * 60 * 24)
);
return {
...e,
daysUntil,
isOverdue: daysUntil < 0,
isUrgent: daysUntil >= 0 && daysUntil <= 7
};
})
.filter(e => e.isOverdue || e.isUrgent)
.sort((a, b) => a.daysUntil - b.daysUntil);
}
维护提醒等级:
| 等级 | 条件 | 提示颜色 | 优先级 |
|---|---|---|---|
| 逾期 | daysUntil < 0 | 红色 | P0 |
| 紧急 | 0 <= daysUntil <= 3 | 橙色 | P1 |
| 预警 | 4 <= daysUntil <= 7 | 黄色 | P2 |
五、控制器设计
5.1 控制器职责
javascript
class EquipmentController {
constructor() {
this.model = new EquipmentModel();
this.initDOMReferences();
this.bindEventListeners();
this.render();
}
}
控制器核心职责:
| 职责 | 方法 | 说明 |
|---|---|---|
| DOM 引用初始化 | initDOMReferences() |
缓存 DOM 元素引用 |
| 事件绑定 | bindEventListeners() |
注册事件监听器 |
| 渲染协调 | render() |
协调各模块渲染 |
| 表单处理 | handleSave() |
处理新增/编辑表单提交 |
| 搜索处理 | handleSearch() |
处理搜索请求 |
| 使用记录 | handleRecordUsage() |
处理使用记录 |
5.2 事件处理机制
javascript
bindEventListeners() {
// 搜索
this.searchBtn.addEventListener('click', () => this.handleSearch());
this.searchInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') this.handleSearch();
});
// 筛选
this.categoryFilter.addEventListener('change', () => this.render());
this.statusFilter.addEventListener('change', () => this.render());
// 添加按钮
this.addBtn.addEventListener('click', () => this.openAddModal());
// 器材表单
this.equipmentForm.addEventListener('submit', (e) => {
e.preventDefault();
this.handleSave();
});
}
5.3 渲染策略
javascript
render() {
// 获取筛选条件
const category = this.categoryFilter.value;
const status = this.statusFilter.value;
// 获取数据
let equipment = this.model.getAll();
// 应用筛选
if (category !== 'all') {
equipment = equipment.filter(e => e.category === category);
}
if (status !== 'all') {
equipment = equipment.filter(e => e.status === status);
}
// 渲染各模块
this.renderEquipmentList(equipment);
this.renderStats();
this.renderTopUsage();
this.renderMaintenanceAlerts();
}
渲染优化:
| 优化点 | 实现方式 | 效果 |
|---|---|---|
| DOM 缓存 | 初始化时缓存所有 DOM 引用 | 减少 DOM 查询 |
| 条件渲染 | 器材列表为空时显示空状态 | 提升用户体验 |
| 批量更新 | 一次性生成 HTML 字符串 | 减少重绘次数 |
六、卡片组件设计
6.1 器材卡片模板
javascript
renderEquipmentCard(item) {
const categoryNames = {
cardio: '有氧器材',
strength: '力量器材',
freeweight: '自由重量',
stretch: '拉伸器材',
other: '其他器材'
};
const statusNames = {
available: '可用',
inuse: '使用中',
maintenance: '维护中'
};
// 计算维护提醒
const today = new Date();
today.setHours(0, 0, 0, 0);
const nextDate = item.nextMaintenance ?
new Date(item.nextMaintenance) : null;
if (nextDate) nextDate.setHours(0, 0, 0, 0);
const daysUntilMaintenance = nextDate ?
Math.ceil((nextDate - today) / (1000 * 60 * 60 * 24)) : null;
const showMaintenanceWarning =
daysUntilMaintenance !== null && daysUntilMaintenance <= 7;
let warningHtml = '';
if (showMaintenanceWarning) {
if (daysUntilMaintenance < 0) {
warningHtml = `
<div class="maintenance-warning">
⚠️ 维护已逾期 ${Math.abs(daysUntilMaintenance)} 天
</div>`;
} else if (daysUntilMaintenance <= 3) {
warningHtml = `
<div class="maintenance-warning">
⚠️ ${daysUntilMaintenance} 天后需要维护
</div>`;
}
}
return `
<div class="equipment-card" data-id="${item.id}">
<div class="card-header">
<div class="card-title">${item.name}</div>
<span class="status-badge ${item.status}">
${statusNames[item.status]}
</span>
</div>
<span class="card-category">
${categoryNames[item.category]}
</span>
<div class="card-stats">
<span>📊 ${item.usageCount} 次</span>
${item.purchaseDate ?
`<span>📅 ${item.purchaseDate}</span>` : ''}
</div>
${item.notes ?
`<div class="card-info"><div>${item.notes}</div></div>` : ''}
${warningHtml}
<div class="card-actions">
<button class="card-btn primary"
onclick="equipmentController.openRecordUsageModal(${item.id})">
📝 使用
</button>
<button class="card-btn secondary"
onclick="equipmentController.openEditModal(${item.id})">
✏️ 编辑
</button>
<button class="card-btn danger"
onclick="equipmentController.deleteEquipment(${item.id})">
🗑️ 删除
</button>
</div>
</div>
`;
}
6.2 卡片布局设计
css
.equipment-card {
background: rgba(255, 255, 255, 0.95);
border-radius: 16px;
padding: 20px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
}
.equipment-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.15);
}
卡片信息层级:
| 层级 | 内容 | 视觉权重 |
|---|---|---|
| 1 | 器材名称、状态标签 | 高(标题级) |
| 2 | 器材分类、统计数据 | 中(信息级) |
| 3 | 备注、维护警告 | 低(辅助级) |
| 4 | 操作按钮 | 功能入口 |
七、弹窗组件设计
7.1 添加/编辑弹窗
javascript
openAddModal() {
this.modalTitle.textContent = '添加器材';
document.getElementById('equipmentId').value = '';
this.equipmentForm.reset();
this.equipmentModal.classList.add('active');
}
openEditModal(id) {
const equipment = this.model.getById(id);
if (!equipment) return;
this.modalTitle.textContent = '编辑器材';
document.getElementById('equipmentId').value = equipment.id;
document.getElementById('name').value = equipment.name;
document.getElementById('category').value = equipment.category;
document.getElementById('status').value = equipment.status;
// ... 其他字段
this.equipmentModal.classList.add('active');
}
7.2 使用记录弹窗
javascript
openRecordUsageModal(id) {
const equipment = this.model.getById(id);
if (!equipment) return;
document.getElementById('usageEquipmentId').value = id;
this.usageEquipmentName.textContent = `正在记录: ${equipment.name}`;
document.getElementById('usageDuration').value = '30';
document.getElementById('usageDate').value =
new Date().toISOString().split('T')[0];
this.usageModal.classList.add('active');
}
7.3 弹窗样式
css
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.6);
backdrop-filter: blur(4px);
z-index: 1000;
align-items: center;
justify-content: center;
}
.modal.active {
display: flex;
}
.modal-content {
background: #fff;
border-radius: 16px;
width: 100%;
max-width: 500px;
max-height: 90vh;
overflow-y: auto;
animation: modalSlideIn 0.3s ease;
}
@keyframes modalSlideIn {
from {
opacity: 0;
transform: translateY(-20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
八、统计面板设计
8.1 统计数据展示
javascript
renderStats() {
const stats = this.model.getStats();
this.totalEquipment.textContent = stats.total;
this.availableCount.textContent = stats.available;
this.inuseCount.textContent = stats.inuse;
this.maintenanceCount.textContent = stats.maintenance;
}
统计面板布局:
css
.stats-list {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 15px;
}
.stat-item {
display: flex;
align-items: center;
gap: 12px;
padding: 15px;
background: #f8f9fa;
border-radius: 12px;
}
.stat-value {
font-size: 1.8rem;
font-weight: 700;
font-family: 'Courier New', monospace;
}
.stat-value.available { color: #28a745; }
.stat-value.inuse { color: #ffc107; }
.stat-value.maintenance { color: #dc3545; }
8.2 TOP 5 排行榜
javascript
renderTopUsage() {
const top = this.model.getTopUsage();
if (top.length === 0) {
this.topEquipment.innerHTML =
'<p style="color: #888; text-align: center;">暂无数据</p>';
return;
}
this.topEquipment.innerHTML = top.map((item, index) => `
<div class="top-item">
<div class="top-rank">${index + 1}</div>
<div class="top-name">${item.name}</div>
<div class="top-count">${item.usageCount}次</div>
</div>
`).join('');
}
九、持久化存储
9.1 localStorage 策略
javascript
saveData() {
localStorage.setItem('fitnessEquipment',
JSON.stringify(this.equipment));
}
loadData() {
const saved = localStorage.getItem('fitnessEquipment');
if (saved) {
try {
this.equipment = JSON.parse(saved);
} catch (e) {
// JSON 解析失败,使用默认数据
this.equipment = this.getDefaultData();
}
} else {
// 首次使用,初始化默认数据
this.equipment = this.getDefaultData();
this.saveData();
}
}
9.2 数据安全处理
javascript
// 异常处理
try {
this.equipment = JSON.parse(saved);
} catch (e) {
console.error('数据解析失败:', e);
this.equipment = this.getDefaultData();
}
// 数据备份建议
// 1. 定期导出 JSON 文件
// 2. 实现数据版本管理
// 3. 添加数据校验机制
十、扩展性设计
10.1 可配置化
javascript
// 分类配置
const CATEGORY_CONFIG = {
cardio: {
name: '有氧器材',
icon: '🏃',
maintenanceInterval: 30 // 天
},
strength: {
name: '力量器材',
icon: '🏋️',
maintenanceInterval: 60
},
// ...
};
10.2 扩展功能建议
| 功能 | 优先级 | 实现难度 | 说明 |
|---|---|---|---|
| 数据导出 | 高 | 低 | CSV/Excel 导出 |
| 二维码管理 | 中 | 中 | 器材扫码识别 |
| 预约系统 | 中 | 中 | 器材使用预约 |
| 使用分析 | 中 | 中 | 图表统计分析 |
| 权限管理 | 低 | 高 | 多用户权限控制 |
| 移动端适配 | 高 | 低 | 响应式优化 |
十一、总结
11.1 架构设计亮点
| 设计点 | 实现方式 | 优势 |
|---|---|---|
| MVC 模式 | Model/View/Controller 分离 | 职责清晰,易于维护 |
| 数据驱动 | localStorage 持久化 | 数据不丢失 |
| 事件委托 | Controller 统一处理 | 减少事件绑定 |
| 组件化 | 卡片、弹窗独立 | 可复用性强 |
11.2 性能优化
| 优化项 | 实现方式 | 效果 |
|---|---|---|
| DOM 缓存 | 初始化时缓存引用 | 减少查询次数 |
| 批量渲染 | 一次性生成 HTML | 减少重绘 |
| 条件渲染 | 空状态独立显示 | 提升首屏速度 |
11.3 用户体验
| 特性 | 实现方式 | 价值 |
|---|---|---|
| 实时反馈 | Toast 提示 | 操作确认 |
| 智能提醒 | 维护预警系统 | 预防性维护 |
| 视觉反馈 | 状态颜色区分 | 信息直观 |
| 快捷操作 | 键盘支持 | 提升效率 |
健身室器材管理系统通过现代化的前端架构设计,实现了器材管理的数字化、智能化,提升了健身房运营效率,降低了管理成本。