如果你想做一个乐高MOC管理App,需要用到Preferences和零件库存
想要高效管理你的乐高MOC项目和零件库存?打开鸿蒙应用市场,搜索「积木图」下载体验,让每一个零件都清清楚楚。
写在前面
玩乐高MOC(My Own Creation,自己设计创作)的人都知道,最头疼的事情不是设计本身,而是零件管理。你设计了一个超酷的模型,打开零件清单一看——需要24个2x4红色砖块,你手里只有18个;需要6个科技系列连接销,你一个都没有。要么临时去买,要么改设计,都很麻烦。
「积木图」这个App就是帮你管这些的。你把MOC项目的零件清单录入进去,App自动对比你的库存,标记出缺件。每个步骤分页展示,用到哪些零件一目了然。零件库存也可以单独管理,买新零件了就更新数量。
这篇文章重点聊:零件库存的数据结构设计、用量统计和缺件计算、步骤分页的展示逻辑。
这篇文章聊什么
- 零件数据模型——零件编号、名称、颜色、类别、库存数量
- MOC项目数据结构——步骤列表、每个步骤的零件清单
- 用量统计——统计整个项目需要多少个每种零件
- 缺件计算——需求量减去库存量,标记缺件
- 步骤分页展示——每个步骤单独一页,显示该步骤需要的零件
第一步:零件数据模型
乐高零件的标识系统很规范。每个零件有一个唯一的零件编号(Part Number),加上颜色就确定了具体的零件。比如 "3001" 是2x4砖块,"3001 Red" 就是红色2x4砖块。
React版本的零件数据
// 零件类别
const PART_CATEGORIES = [
'brick', // 砖块
'plate', // 板
'tile', // 光面板
'slope', // 斜面
'technic', // 科技系列
'minifig', // 人仔
'accessory', // 配件
'other', // 其他
];
// 库存中的一个零件
const createPart = () => ({
partId: '', // 零件编号,如 "3001"
name: '', // 名称,如 "Brick 2x4"
color: '', // 颜色,如 "Red"
category: 'brick', // 类别
quantity: 0, // 库存数量
imageUrl: '', // 零件图片URL(可选)
});
// MOC项目的一个步骤
const createStep = () => ({
stepNumber: 1,
description: '',
parts: [], // 该步骤需要的零件 [{partId, color, quantity}]
notes: '',
});
// MOC项目
const createMOCProject = () => ({
id: Date.now(),
name: '',
author: '',
steps: [],
totalParts: 0, // 总零件种类数
totalPieces: 0, // 总零件个数
missingParts: [], // 缺件列表
createdAt: new Date().toISOString(),
});
parts 数组里的每个元素是 partId + color + quantity。为什么需要颜色?因为同一个零件编号可能有多种颜色,红色2x4砖块和蓝色2x4砖块是不同的零件。
ArkTS版本的零件数据模型
// 零件类别
enum PartCategory {
BRICK = 'brick',
PLATE = 'plate',
TILE = 'tile',
SLOPE = 'slope',
TECHNIC = 'technic',
MINIFIG = 'minifig',
ACCESSORY = 'accessory',
OTHER = 'other',
}
// 库存零件
interface InventoryPart {
partId: string;
name: string;
color: string;
category: PartCategory;
quantity: number;
imageUrl: string;
}
// 步骤中的零件需求
interface StepPartNeed {
partId: string;
color: string;
quantity: number; // 需要的数量
}
// MOC步骤
interface MOCStep {
stepNumber: number;
description: string;
parts: StepPartNeed[];
notes: string;
}
// 缺件信息
interface MissingPart {
partId: string;
color: string;
needed: number; // 需要数量
have: number; // 库存数量
short: number; // 缺少数量
}
// MOC项目
interface MOCProject {
id: number;
name: string;
author: string;
steps: MOCStep[];
totalParts: number;
totalPieces: number;
missingParts: MissingPart[];
createdAt: string;
}
MissingPart 接口比 StepPartNeed 多了两个字段:have(库存数量)和 short(缺少数量)。short = needed - have,如果 short > 0 就说明缺件。
第二步:用量统计——汇总整个项目的零件需求
一个MOC项目可能有几十个步骤,每个步骤用到不同的零件。我们需要把所有步骤的零件需求汇总起来,统计每种零件的总需求量。
React版本:用量统计
function calculatePartUsage(steps) {
const usageMap = {};
steps.forEach((step) => {
step.parts.forEach((part) => {
// 用 partId + color 作为唯一标识
const key = `${part.partId}_${part.color}`;
if (!usageMap[key]) {
usageMap[key] = {
partId: part.partId,
color: part.color,
totalNeeded: 0,
usedInSteps: [],
};
}
usageMap[key].totalNeeded += part.quantity;
usageMap[key].usedInSteps.push(step.stepNumber);
});
});
return Object.values(usageMap);
}
usedInSteps 记录了每种零件在哪些步骤中被使用。这个信息很有用——如果你发现某个零件缺了,你就知道它影响的是哪些步骤,可以优先处理。
ArkTS版本:用量统计
interface PartUsage {
partId: string;
color: string;
totalNeeded: number;
usedInSteps: number[];
}
function calculatePartUsage(steps: MOCStep[]): PartUsage[] {
const usageMap: Map<string, PartUsage> = new Map();
steps.forEach((step) => {
step.parts.forEach((part) => {
const key = `${part.partId}_${part.color}`;
if (usageMap.has(key)) {
const existing = usageMap.get(key)!;
existing.totalNeeded += part.quantity;
existing.usedInSteps.push(step.stepNumber);
} else {
usageMap.set(key, {
partId: part.partId,
color: part.color,
totalNeeded: part.quantity,
usedInSteps: [step.stepNumber],
});
}
});
});
return Array.from(usageMap.values());
}
ArkTS版本用了 Map 来做汇总,比用普通对象更规范。Map.get(key)! 后面的感叹号是非空断言,因为我们已经通过 has 确认了key存在,所以这里不会是undefined。
第三步:缺件计算
有了用量统计和库存数据,缺件计算就是一一对比。
React版本:缺件计算
function calculateMissingParts(partUsage, inventory) {
const missing = [];
partUsage.forEach((usage) => {
// 在库存中查找匹配的零件
const invPart = inventory.find(
(p) => p.partId === usage.partId && p.color === usage.color
);
const have = invPart ? invPart.quantity : 0;
const short = usage.totalNeeded - have;
if (short > 0) {
missing.push({
partId: usage.partId,
color: usage.color,
needed: usage.totalNeeded,
have,
short,
});
}
});
// 按缺少数量从大到小排序
missing.sort((a, b) => b.short - a.short);
return missing;
}
缺件按缺少数量从大到小排序,这样最紧缺的零件排在前面,用户一眼就能看到最需要优先购买的零件。
ArkTS版本:缺件计算
function calculateMissingParts(
partUsage: PartUsage[],
inventory: InventoryPart[]
): MissingPart[] {
const missing: MissingPart[] = [];
partUsage.forEach((usage) => {
const invPart = inventory.find(
(p) => p.partId === usage.partId && p.color === usage.color
);
const have = invPart ? invPart.quantity : 0;
const short = usage.totalNeeded - have;
if (short > 0) {
missing.push({
partId: usage.partId,
color: usage.color,
needed: usage.totalNeeded,
have,
short,
});
}
});
// 按缺少数量排序
missing.sort((a, b) => b.short - a.short);
return missing;
}
逻辑和React版本完全一样。这里有个优化空间:如果零件很多,用 find 去遍历库存数组效率不高。更好的做法是先把库存转成一个Map,用 partId_color 作为key,这样查找就是O(1)了。但对于几十到几百个零件的规模,find 完全够用。
第四步:步骤分页展示
MOC的搭建说明通常是分步骤的。每个步骤展示需要的零件和操作说明。我们用分页(Swiper)组件来实现。
ArkTS版本:步骤分页
@Component
struct StepPageView {
steps: MOCStep[] = [];
inventory: InventoryPart[] = [];
build() {
Swiper() {
ForEach(this.steps, (step: MOCStep) => {
this.StepContent(step);
}, (step: MOCStep) => `step_${step.stepNumber}`);
}
.index(0)
.autoPlay(false)
.indicator(true)
.loop(false)
.width('100%')
.height('100%');
}
@Builder
StepContent(step: MOCStep) {
Scroll() {
Column({ space: 16 }) {
// 步骤标题
Row() {
Text(`步骤 ${step.stepNumber}`)
.fontSize(22)
.fontWeight(FontWeight.Bold)
.fontColor('#E65100');
Text(`/ 共 ${this.steps.length} 步`)
.fontSize(14)
.fontColor('#999999')
.margin({ left: 8 });
}
.width('100%');
// 步骤描述
if (step.description) {
Text(step.description)
.fontSize(14)
.fontColor('#666666')
.width('100%');
}
// 该步骤的零件列表
Text('本步骤零件')
.fontSize(16)
.fontWeight(FontWeight.Medium)
.margin({ top: 8 });
ForEach(step.parts, (part: StepPartNeed) => {
this.PartRow(part);
}, (part: StepPartNeed) => `${part.partId}_${part.color}`);
// 备注
if (step.notes) {
Text(step.notes)
.fontSize(13)
.fontColor('#999999')
.fontStyle(FontStyle.Italic)
.margin({ top: 8 });
}
}
.padding(20);
}
.scrollable(ScrollDirection.Vertical);
}
@Builder
PartRow(part: StepPartNeed) {
Row({ space: 12 }) {
// 零件颜色色块
Column()
.width(32)
.height(32)
.borderRadius(4)
.backgroundColor(this.getColorHex(part.color))
.borderWidth(1)
.borderColor('#E0E0E0');
// 零件信息
Column({ space: 2 }) {
Text(part.partId)
.fontSize(14)
.fontWeight(FontWeight.Medium);
Text(part.color)
.fontSize(12)
.fontColor('#999999');
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start);
// 需要数量
Text(`x${part.quantity}`)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#333333');
}
.width('100%')
.padding(12)
.backgroundColor('#FAFAFA')
.borderRadius(8);
}
// 颜色名称转十六进制(简化版,实际应用中应该用完整的映射表)
getColorHex(colorName: string): string {
const colorMap: Record<string, string> = {
'Red': '#B71C1C',
'Blue': '#0D47A1',
'Green': '#1B5E20',
'Yellow': '#F57F17',
'White': '#FAFAFA',
'Black': '#212121',
'Orange': '#E65100',
'Gray': '#757575',
'Light Gray': '#BDBDBD',
'Brown': '#4E342E',
'Tan': '#D7CCC8',
'Dark Blue': '#1A237E',
'Dark Green': '#004D40',
'Lime': '#827717',
'Pink': '#C2185B',
'Purple': '#4A148C',
};
return colorMap[colorName] || '#9E9E9E';
}
}
Swiper 是鸿蒙的分页滑动组件,非常适合步骤浏览的场景。loop: false 禁止循环滑动——到最后一步就不能再往右滑了,符合步骤的线性逻辑。indicator: true 显示底部的页面指示器,让用户知道当前在第几步。
getColorHex 是一个颜色名称到十六进制值的映射。实际应用中,乐高的颜色名称有上百种,这里只列了常用的十几种。完整的映射可以维护在一个单独的配置文件中。
第五步:库存管理和缺件标记
ArkTS版本:缺件标记组件
@Component
struct MissingPartsPanel {
missingParts: MissingPart[] = [];
build() {
Column({ space: 12 }) {
Text('缺件清单')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#D32F2F');
if (this.missingParts.length === 0) {
Text('零件齐全,可以开始搭建')
.fontSize(14)
.fontColor('#4CAF50')
.textAlign(TextAlign.Center)
.width('100%')
.padding(24);
} else {
Text(`共缺 ${this.missingParts.length} 种零件`)
.fontSize(14)
.fontColor('#FF5252');
ForEach(this.missingParts, (part: MissingPart) => {
Row({ space: 12 }) {
Column({ space: 2 }) {
Text(`${part.partId} (${part.color})`)
.fontSize(14)
.fontWeight(FontWeight.Medium);
Text(`需要 ${part.needed} 个,库存 ${part.have} 个`)
.fontSize(12)
.fontColor('#666666');
}
.layoutWeight(1);
// 缺少数量,红色高亮
Text(`缺 ${part.short}`)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#D32F2F')
.padding({ left: 8, right: 8, top: 4, bottom: 4 })
.backgroundColor('#FFEBEE')
.borderRadius(4);
}
.width('100%')
.padding(12)
.backgroundColor('#FFF5F5')
.borderRadius(8);
}, (part: MissingPart) => `${part.partId}_${part.color}`);
}
}
.width('100%')
.padding(16);
}
}
缺件面板用红色系配色来强调紧迫感。每种缺件显示零件编号、颜色、需要数量、库存数量和缺少数量。如果零件齐全,显示绿色的"零件齐全"提示。
流程图
flowchart TD
A[打开积木图] --> B{选择操作}
B -->|新建MOC项目| C[输入项目名称]
B -->|管理库存| D[添加/编辑零件库存]
B -->|查看项目| E[加载已有项目]
C --> F[添加搭建步骤]
F --> G[为每个步骤添加零件需求]
G --> H[保存项目]
H --> I[统计零件用量]
I --> J[对比库存计算缺件]
J --> K{有缺件?}
K -->|是| L[显示缺件清单]
K -->|否| M[显示零件齐全]
L --> N[按缺少数量排序]
N --> O[用户购买缺件]
O --> P[更新库存数量]
P --> Q[重新计算缺件]
E --> R[分页浏览步骤]
R --> S[查看每步零件需求]
S --> T[标记已完成的步骤]
React vs ArkTS 对比表
| 对比项 | React (Web) | ArkTS (鸿蒙) |
|---|---|---|
| 分页组件 | 轮播库或CSS scroll-snap | Swiper组件 |
| 零件列表 | map + div | ForEach + Row/Column |
| 颜色映射 | JS对象 | Record类型 |
| 数据汇总 | reduce + 对象 | Map + 遍历 |
| 缺件排序 | Array.sort | 同React |
| 数据存储 | localStorage | Preferences |
总结
这篇文章我们实现了一个乐高MOC管理App的核心功能。几个要点:
- 零件用 partId + color 唯一标识:同一个零件编号可能有多种颜色,必须组合起来才能确定具体零件。
- 用量统计用Map汇总:遍历所有步骤的零件需求,按 partId_color 累加数量,同时记录使用了哪些步骤。
- 缺件计算是需求减库存:简单的减法,但要注意库存中找不到的零件视为库存为0。
- 步骤分页用Swiper:鸿蒙的Swiper组件天然适合步骤浏览,禁止循环和自动播放。
- 缺件按紧缺程度排序:缺少最多的排在前面,方便用户优先采购。
乐高MOC的乐趣在于创造,但创造的基础是零件管理到位。希望积木图能帮你把零件管得明明白白,把精力集中在设计上。