十字绣图案数据怎么存?HarmonyOS preferences管理图案数据

如果你对十字绣感兴趣,可以去鸿蒙应用市场搜一下**「绣迹」**,下载下来体验体验。创建图案、编辑网格、保存进度,一套流程走下来对十字绣的数字化创作会有更直观的理解。体验完了再回来看这篇文章,你会更清楚这些图案数据是怎么存储和管理的。


写在前面

大家好,我是一名写了十多年Web前端的老兵。从jQuery时代一路走到React/Vue,CSS3动画、requestAnimationFrame、Web Animation API这些都算是看家本领。去年开始转战鸿蒙生态,用ArkTS开发App,这一路踩了不少坑,也积累了不少心得。

很多人觉得"前端转鸿蒙"应该很容易——都是写UI嘛,组件化、状态管理、生命周期,概念都差不多。但真正上手之后你会发现,相似的地方让你觉得亲切,不同的地方让你抓狂

比如:

  • 大数据存储:十字绣图案的网格数据是二维数组,直接存preferences会很大,需要考虑压缩或分块存储。
  • 状态同步:编辑器和列表页之间要同步数据,需要考虑页面间通信。

别担心,接下来这篇文章,我会用"绣迹"的图案管理功能,带你看看怎么在HarmonyOS里管理复杂的图案数据。


这篇文章聊什么

绣迹的图案管理功能,核心要解决:

  1. 图案列表:展示所有保存的图案
  2. 图案详情:显示图案信息和预览
  3. 图案操作:重命名、复制、删除
  4. 进度追踪:记录绣制进度

第一步:设计图案数据结构

// 十字绣图案
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会很慢。

解决方案

  1. 压缩数据:只存非空格子
  2. 分块存储:将大数组分成多个小块
// 压缩存储:只存非空格子
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设计

  • 图案列表和筛选
  • 详情页面和操作按钮
  • 进度条可视化

注意事项

  • 大型图案要考虑数据压缩
  • 状态更新要自动同步
  • 提供数据备份功能

如果你对"绣迹"感兴趣,欢迎去鸿蒙应用市场搜索下载体验。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐