鸿蒙App开发--绣迹图案数据怎么存?HarmonyOS preferences管理图案数据
·
十字绣图案数据怎么存?HarmonyOS preferences管理图案数据
如果你对十字绣感兴趣,可以去鸿蒙应用市场搜一下**「绣迹」**,下载下来体验体验。创建图案、编辑网格、保存进度,一套流程走下来对十字绣的数字化创作会有更直观的理解。体验完了再回来看这篇文章,你会更清楚这些图案数据是怎么存储和管理的。
写在前面
大家好,我是一名写了十多年Web前端的老兵。从jQuery时代一路走到React/Vue,CSS3动画、requestAnimationFrame、Web Animation API这些都算是看家本领。去年开始转战鸿蒙生态,用ArkTS开发App,这一路踩了不少坑,也积累了不少心得。
很多人觉得"前端转鸿蒙"应该很容易——都是写UI嘛,组件化、状态管理、生命周期,概念都差不多。但真正上手之后你会发现,相似的地方让你觉得亲切,不同的地方让你抓狂。
比如:
- 大数据存储:十字绣图案的网格数据是二维数组,直接存preferences会很大,需要考虑压缩或分块存储。
- 状态同步:编辑器和列表页之间要同步数据,需要考虑页面间通信。
别担心,接下来这篇文章,我会用"绣迹"的图案管理功能,带你看看怎么在HarmonyOS里管理复杂的图案数据。
这篇文章聊什么
绣迹的图案管理功能,核心要解决:
- 图案列表:展示所有保存的图案
- 图案详情:显示图案信息和预览
- 图案操作:重命名、复制、删除
- 进度追踪:记录绣制进度
第一步:设计图案数据结构
// 十字绣图案
interface CrossStitchPattern {
id: string;
name: string;
width: number;
height: number;
grid: string[][]; // 颜色ID二维数组
fabricType: string; // 绣布类型
status: string; // 状态:planning, in_progress, completed
completedStitches: number; // 已完成针数
totalStitches: number; // 总针数
notes: string;
createdAt: string;
updatedAt: string;
}
// 绣布类型
const FABRIC_TYPES = [
{ id: 'aida_14', name: 'Aida 14CT', count: 14 },
{ id: 'aida_16', name: 'Aida 16CT', count: 16 },
{ id: 'aida_18', name: 'Aida 18CT', count: 18 },
{ id: 'evenweave_25', name: '均匀织 25CT', count: 25 },
{ id: 'linen_28', name: '亚麻 28CT', count: 28 },
];
// 项目状态
const PROJECT_STATUS = [
{ id: 'planning', name: '规划中', color: '#6b7280', icon: '📋' },
{ id: 'in_progress', name: '进行中', color: '#3b82f6', icon: '✂️' },
{ id: 'paused', name: '暂停', color: '#f97316', icon: '⏸️' },
{ id: 'completed', name: '已完成', color: '#22c55e', icon: '✅' },
{ id: 'framed', name: '已装裱', color: '#8b5cf6', icon: '🖼️' },
];
第二步:实现图案的CRUD操作
import { preferences } from '@kit.ArkData';
let prefInstance: preferences.Preferences | null = null;
async function getPreferences(context: Context): Promise<preferences.Preferences> {
if (!prefInstance) {
prefInstance = await preferences.getPreferences(context, 'xiuji_data');
}
return prefInstance;
}
async function setItem(context: Context, key: string, value: unknown): Promise<boolean> {
try {
const pref = await getPreferences(context);
await pref.put(key, JSON.stringify(value));
await pref.flush();
return true;
} catch (err) {
console.error('存储失败:', err);
return false;
}
}
async function getItem<T>(context: Context, key: string, defaultValue: T): Promise<T> {
try {
const pref = await getPreferences(context);
const value = await pref.get(key, '');
if (typeof value === 'string' && value.length > 0) {
return JSON.parse(value) as T;
}
return defaultValue;
} catch (err) {
console.error('读取失败:', err);
return defaultValue;
}
}
// 添加图案
async function addPattern(context: Context, pattern: CrossStitchPattern): Promise<boolean> {
const patterns = await getItem<CrossStitchPattern[]>(context, 'patterns', []);
patterns.push({
...pattern,
id: `pattern_${Date.now()}`,
createdAt: new Date().toISOString().slice(0, 10),
updatedAt: new Date().toISOString().slice(0, 10)
});
return await setItem(context, 'patterns', patterns);
}
// 获取所有图案
async function getPatterns(context: Context): Promise<CrossStitchPattern[]> {
return await getItem<CrossStitchPattern[]>(context, 'patterns', []);
}
// 更新图案
async function updatePattern(context: Context, id: string, updates: Partial<CrossStitchPattern>): Promise<boolean> {
const patterns = await getItem<CrossStitchPattern[]>(context, 'patterns', []);
const index = patterns.findIndex(p => p.id === id);
if (index === -1) return false;
patterns[index] = {
...patterns[index],
...updates,
updatedAt: new Date().toISOString().slice(0, 10)
};
return await setItem(context, 'patterns', patterns);
}
// 删除图案
async function deletePattern(context: Context, id: string): Promise<boolean> {
const patterns = await getItem<CrossStitchPattern[]>(context, 'patterns', []);
const filtered = patterns.filter(p => p.id !== id);
return await setItem(context, 'patterns', filtered);
}
// 复制图案
async function duplicatePattern(context: Context, id: string): Promise<boolean> {
const patterns = await getItem<CrossStitchPattern[]>(context, 'patterns', []);
const original = patterns.find(p => p.id === id);
if (!original) return false;
const copy: CrossStitchPattern = {
...original,
id: `pattern_${Date.now()}`,
name: `${original.name} (副本)`,
status: 'planning',
completedStitches: 0,
createdAt: new Date().toISOString().slice(0, 10),
updatedAt: new Date().toISOString().slice(0, 10)
};
patterns.push(copy);
return await setItem(context, 'patterns', patterns);
}
第三步:计算绣制进度
// 计算进度百分比
function calculateProgress(pattern: CrossStitchPattern): number {
if (pattern.totalStitches === 0) return 0;
return Math.min(100, Math.round((pattern.completedStitches / pattern.totalStitches) * 100));
}
// 计算绣布尺寸(毫米)
function calculateFabricSize(
width: number,
height: number,
fabricType: string,
border: number = 2
): { width: number; height: number } {
const fabric = FABRIC_TYPES.find(f => f.id === fabricType);
if (!fabric) return { width: 0, height: 0 };
const stitchSize = 25.4 / fabric.count; // 每针的毫米数
return {
width: Math.round((width + border * 2) * stitchSize),
height: Math.round((height + border * 2) * stitchSize)
};
}
// 估算完成时间(小时)
function estimateCompletionTime(
totalStitches: number,
completedStitches: number,
stitchesPerHour: number = 100
): number {
const remaining = totalStitches - completedStitches;
return Math.round(remaining / stitchesPerHour);
}
// 更新进度
async function updateProgress(
context: Context,
patternId: string,
completedStitches: number
): Promise<boolean> {
const patterns = await getItem<CrossStitchPattern[]>(context, 'patterns', []);
const index = patterns.findIndex(p => p.id === patternId);
if (index === -1) return false;
patterns[index].completedStitches = completedStitches;
// 自动更新状态
if (completedStitches >= patterns[index].totalStitches) {
patterns[index].status = 'completed';
} else if (completedStitches > 0) {
patterns[index].status = 'in_progress';
}
patterns[index].updatedAt = new Date().toISOString().slice(0, 10);
return await setItem(context, 'patterns', patterns);
}
第四步:图案列表页面
@Entry
@Component
struct PatternListPage {
@State patterns: CrossStitchPattern[] = []
@State filter: string = 'all'
async aboutToAppear() {
this.patterns = await getPatterns(getContext(this) as Context);
}
get filteredPatterns(): CrossStitchPattern[] {
if (this.filter === 'all') return this.patterns;
return this.patterns.filter(p => p.status === this.filter);
}
build() {
Column() {
Text('我的图案')
.fontSize(24)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 16 })
// 筛选标签
Scroll() {
Row() {
ForEach(PROJECT_STATUS, (status) => {
Button(status.name)
.fontSize(12)
.height(32)
.backgroundColor(this.filter === status.id ? status.color : '#f3f4f6')
.fontColor(this.filter === status.id ? '#ffffff' : '#374151')
.borderRadius(16)
.margin({ right: 8 })
.onClick(() => { this.filter = status.id; })
})
}
}
.scrollable(ScrollDirection.Horizontal)
.width('100%')
.margin({ bottom: 16 })
// 图案列表
ForEach(this.filteredPatterns, (pattern: CrossStitchPattern) => {
Row() {
// 预览缩略图(简化版)
Canvas(new CanvasRenderingContext2D(new RenderingContextSettings(true)))
.width(60)
.height(60)
.backgroundColor('#fef9c3')
.borderRadius(8)
// 图案信息
Column() {
Text(pattern.name)
.fontSize(16)
.fontWeight(FontWeight.Medium)
Text(`${pattern.width}×${pattern.height} · ${pattern.fabricType}`)
.fontSize(12)
.fontColor('#9ca3af')
// 进度条
Row() {
Stack({ alignContent: Alignment.Start }) {
Row()
.width('100%')
.height(4)
.backgroundColor('#f3f4f6')
.borderRadius(2)
Row()
.width(`${calculateProgress(pattern)}%`)
.height(4)
.backgroundColor('#22c55e')
.borderRadius(2)
}
.layoutWeight(1)
Text(`${calculateProgress(pattern)}%`)
.fontSize(12)
.fontColor('#6b7280')
.margin({ left: 8 })
}
.margin({ top: 8 })
}
.layoutWeight(1)
.margin({ left: 12 })
.alignItems(HorizontalAlign.Start)
}
.width('100%')
.padding(12)
.backgroundColor('#ffffff')
.borderRadius(12)
.margin({ bottom: 8 })
})
}
.padding(16)
}
}
第五步:图案详情页面
@Entry
@Component
struct PatternDetailPage {
@State pattern: CrossStitchPattern | null = null
@State showRenameDialog: boolean = false
@State newName: string = ''
async aboutToAppear() {
// 从路由参数获取图案ID
const patternId = 'pattern_123'; // 实际从路由获取
const patterns = await getPatterns(getContext(this) as Context);
this.pattern = patterns.find(p => p.id === patternId) || null;
}
private async handleDelete() {
if (!this.pattern) return;
const success = await deletePattern(getContext(this) as Context, this.pattern.id);
if (success) {
router.back();
}
}
private async handleDuplicate() {
if (!this.pattern) return;
const success = await duplicatePattern(getContext(this) as Context, this.pattern.id);
if (success) {
// 刷新列表
}
}
private async handleRename() {
if (!this.pattern || !this.newName.trim()) return;
const success = await updatePattern(getContext(this) as Context, this.pattern.id, {
name: this.newName.trim()
});
if (success) {
this.pattern.name = this.newName.trim();
this.showRenameDialog = false;
}
}
build() {
Column() {
if (this.pattern) {
Text(this.pattern.name)
.fontSize(24)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 16 })
// 预览图
Canvas(new CanvasRenderingContext2D(new RenderingContextSettings(true)))
.width('100%')
.height(300)
.backgroundColor('#fef9c3')
.borderRadius(12)
// 信息卡片
Column() {
Row() {
Text('尺寸')
.fontColor('#9ca3af')
Text(`${this.pattern.width}×${this.pattern.height}`)
.fontWeight(FontWeight.Medium)
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
Row() {
Text('绣布')
.fontColor('#9ca3af')
Text(this.pattern.fabricType)
.fontWeight(FontWeight.Medium)
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
Row() {
Text('进度')
.fontColor('#9ca3af')
Text(`${calculateProgress(this.pattern)}%`)
.fontWeight(FontWeight.Medium)
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
Row() {
Text('状态')
.fontColor('#9ca3af')
Text(PROJECT_STATUS.find(s => s.id === this.pattern?.status)?.name || '')
.fontWeight(FontWeight.Medium)
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
}
.padding(16)
.backgroundColor('#f9fafb')
.borderRadius(12)
.margin({ top: 16 })
// 操作按钮
Column() {
Button('编辑图案')
.width('100%')
.height(48)
.backgroundColor('#3b82f6')
.borderRadius(12)
Row() {
Button('重命名')
.layoutWeight(1)
.height(44)
.backgroundColor('#f3f4f6')
.fontColor('#374151')
.borderRadius(12)
Button('复制')
.layoutWeight(1)
.height(44)
.backgroundColor('#f3f4f6')
.fontColor('#374151')
.borderRadius(12)
.margin({ left: 8 })
Button('删除')
.layoutWeight(1)
.height(44)
.backgroundColor('#fef2f2')
.fontColor('#ef4444')
.borderRadius(12)
.margin({ left: 8 })
}
.width('100%')
.margin({ top: 8 })
}
.margin({ top: 16 })
}
}
.padding(16)
}
}
第六步:常见问题
6.1 网格数据过大
问题:大型图案的grid数组很大,存preferences会很慢。
解决方案:
- 压缩数据:只存非空格子
- 分块存储:将大数组分成多个小块
// 压缩存储:只存非空格子
interface CompressedGrid {
width: number;
height: number;
cells: Array<{ x: number; y: number; color: string }>;
}
function compressGrid(grid: string[][]): CompressedGrid {
const cells: Array<{ x: number; y: number; color: string }> = [];
for (let y = 0; y < grid.length; y++) {
for (let x = 0; x < grid[y].length; x++) {
if (grid[y][x] && grid[y][x] !== 'empty') {
cells.push({ x, y, color: grid[y][x] });
}
}
}
return { width: grid[0]?.length || 0, height: grid.length, cells };
}
function decompressGrid(compressed: CompressedGrid): string[][] {
const grid = Array.from({ length: compressed.height }, () =>
Array.from({ length: compressed.width }, () => 'empty')
);
for (const cell of compressed.cells) {
grid[cell.y][cell.x] = cell.color;
}
return grid;
}
6.2 数据备份
问题:用户换手机后数据丢失。
解决:提供导出/导入功能,将数据导出为JSON文件。
总结
这篇文章围绕"绣迹"的图案管理功能,讲解了:
数据管理
- 图案的CRUD操作
- 进度追踪和状态管理
- 数据压缩优化
UI设计
- 图案列表和筛选
- 详情页面和操作按钮
- 进度条可视化
注意事项
- 大型图案要考虑数据压缩
- 状态更新要自动同步
- 提供数据备份功能
如果你对"绣迹"感兴趣,欢迎去鸿蒙应用市场搜索下载体验。
更多推荐


所有评论(0)