鸿蒙APP开发-带你走近分构App的分子数据

3 阅读5分钟

分构App分子数据管理功能技术解析

继续聊聊分构App,这次是分子数据管理功能。想体验完整功能,可以去鸿蒙应用市场搜索"分构"下载。

写在前面

上一篇我们聊了分构App的Canvas3D功能,今天聊聊分子数据管理——怎么存储、查询和管理分子信息。

化学领域的数据结构比较特殊,分子式、化学键、原子坐标这些都需要专门的数据模型。Web端通常用JSON存储,鸿蒙端用RDB更合适,特别是需要复杂查询的时候。

今天这篇,我会从分子数据模型、数据存储、搜索查询这几个方面,聊聊分构App的数据管理功能。


1. 分子数据模型:更完整的定义

除了基本的原子和化学键,分子数据还需要包含更多信息。

完整分子数据模型:

interface Molecule {
  id: string;
  name: string;
  formula: string; // 分子式,如 H₂O
  molecularWeight: number; // 分子量
  smiles: string; // SMILES表示法
  description: string;
  category: string; // 分类:有机物、无机物等
  tags: string[];
  atoms: Atom[];
  bonds: Bond[];
  properties: MolecularProperty[];
  createdAt: number;
  updatedAt: number;
}

interface Atom {
  id: string;
  element: string;
  atomicNumber: number;
  position: Point3D;
  charge: number; // 电荷
  hybridization: string; // 杂化类型
}

interface Bond {
  id: string;
  atom1Id: string;
  atom2Id: string;
  type: BondType;
  length: number; // 键长
  energy: number; // 键能
}

enum BondType {
  SINGLE = 'single',
  DOUBLE = 'double',
  TRIPLE = 'triple',
  AROMATIC = 'aromatic',
  IONIC = 'ionic'
}

interface MolecularProperty {
  name: string;
  value: number;
  unit: string;
}

// 元素周期表数据
const periodicTable: Record<string, ElementInfo> = {
  'H': { atomicNumber: 1, name: '氢', symbol: 'H', mass: 1.008, electronConfig: '1s¹' },
  'He': { atomicNumber: 2, name: '氦', symbol: 'He', mass: 4.003, electronConfig: '1s²' },
  'Li': { atomicNumber: 3, name: '锂', symbol: 'Li', mass: 6.941, electronConfig: '[He]2s¹' },
  'Be': { atomicNumber: 4, name: '铍', symbol: 'Be', mass: 9.012, electronConfig: '[He]2s²' },
  'B': { atomicNumber: 5, name: '硼', symbol: 'B', mass: 10.81, electronConfig: '[He]2s²2p¹' },
  'C': { atomicNumber: 6, name: '碳', symbol: 'C', mass: 12.01, electronConfig: '[He]2s²2p²' },
  'N': { atomicNumber: 7, name: '氮', symbol: 'N', mass: 14.01, electronConfig: '[He]2s²2p³' },
  'O': { atomicNumber: 8, name: '氧', symbol: 'O', mass: 16.00, electronConfig: '[He]2s²2p⁴' },
  'F': { atomicNumber: 9, name: '氟', symbol: 'F', mass: 19.00, electronConfig: '[He]2s²2p⁵' },
  'Ne': { atomicNumber: 10, name: '氖', symbol: 'Ne', mass: 20.18, electronConfig: '[He]2s²2p⁶' },
  // ... 更多元素
};

interface ElementInfo {
  atomicNumber: number;
  name: string;
  symbol: string;
  mass: number;
  electronConfig: string;
}

2. 分子存储:数据库设计

ArkTS分子数据存储:

import { relationalStore } from '@kit.ArkData';

class MoleculeDatabase {
  private rdbStore: relationalStore.RdbStore | null = null;

  async init(context: Context) {
    const config: relationalStore.StoreConfig = {
      name: 'molecules.db',
      securityLevel: relationalStore.SecurityLevel.S1
    };

    this.rdbStore = await relationalStore.getRdbStore(context, config);

    await this.rdbStore.executeSql(`
      CREATE TABLE IF NOT EXISTS molecules (
        id TEXT PRIMARY KEY,
        name TEXT NOT NULL,
        formula TEXT,
        molecular_weight REAL,
        smiles TEXT,
        description TEXT,
        category TEXT,
        tags TEXT,
        atoms_json TEXT,
        bonds_json TEXT,
        properties_json TEXT,
        created_at INTEGER,
        updated_at INTEGER
      )
    `);

    // 创建索引
    await this.rdbStore.executeSql(`
      CREATE INDEX IF NOT EXISTS idx_molecules_category ON molecules(category)
    `);
    await this.rdbStore.executeSql(`
      CREATE INDEX IF NOT EXISTS idx_molecules_formula ON molecules(formula)
    `);
  }

  // 保存分子
  async saveMolecule(molecule: Molecule): Promise<void> {
    if (!this.rdbStore) return;

    const bucket: relationalStore.ValuesBucket = {
      id: molecule.id,
      name: molecule.name,
      formula: molecule.formula,
      molecular_weight: molecule.molecularWeight,
      smiles: molecule.smiles,
      description: molecule.description,
      category: molecule.category,
      tags: JSON.stringify(molecule.tags),
      atoms_json: JSON.stringify(molecule.atoms),
      bonds_json: JSON.stringify(molecule.bonds),
      properties_json: JSON.stringify(molecule.properties),
      created_at: molecule.createdAt,
      updated_at: Date.now()
    };

    await this.rdbStore.insert('molecules', bucket);
  }

  // 获取分子
  async getMolecule(id: string): Promise<Molecule | null> {
    if (!this.rdbStore) return null;

    const resultSet = await this.rdbStore.querySql(
      'SELECT * FROM molecules WHERE id = ?',
      [id]
    );

    if (resultSet.goToNextRow()) {
      const molecule = this.parseMolecule(resultSet);
      resultSet.close();
      return molecule;
    }

    resultSet.close();
    return null;
  }

  // 搜索分子
  async searchMolecules(keyword: string): Promise<Molecule[]> {
    if (!this.rdbStore) return [];

    const sql = `
      SELECT * FROM molecules
      WHERE name LIKE ? OR formula LIKE ? OR description LIKE ? OR tags LIKE ?
      ORDER BY name
      LIMIT 50
    `;

    const likeKeyword = `%${keyword}%`;
    const resultSet = await this.rdbStore.querySql(sql, [likeKeyword, likeKeyword, likeKeyword, likeKeyword]);

    const molecules: Molecule[] = [];
    while (resultSet.goToNextRow()) {
      molecules.push(this.parseMolecule(resultSet));
    }
    resultSet.close();

    return molecules;
  }

  // 按分类获取
  async getByCategory(category: string): Promise<Molecule[]> {
    if (!this.rdbStore) return [];

    const resultSet = await this.rdbStore.querySql(
      'SELECT * FROM molecules WHERE category = ? ORDER BY name',
      [category]
    );

    const molecules: Molecule[] = [];
    while (resultSet.goToNextRow()) {
      molecules.push(this.parseMolecule(resultSet));
    }
    resultSet.close();

    return molecules;
  }

  // 按元素筛选
  async getByElement(element: string): Promise<Molecule[]> {
    if (!this.rdbStore) return [];

    const resultSet = await this.rdbStore.querySql(
      'SELECT * FROM molecules WHERE atoms_json LIKE ? ORDER BY name',
      [`%"element":"${element}"%`]
    );

    const molecules: Molecule[] = [];
    while (resultSet.goToNextRow()) {
      molecules.push(this.parseMolecule(resultSet));
    }
    resultSet.close();

    return molecules;
  }

  // 获取所有分类
  async getCategories(): Promise<string[]> {
    if (!this.rdbStore) return [];

    const resultSet = await this.rdbStore.querySql(
      'SELECT DISTINCT category FROM molecules ORDER BY category'
    );

    const categories: string[] = [];
    while (resultSet.goToNextRow()) {
      categories.push(resultSet.getString(resultSet.getColumnIndex('category')));
    }
    resultSet.close();

    return categories;
  }

  // 删除分子
  async deleteMolecule(id: string): Promise<void> {
    if (!this.rdbStore) return;

    await this.rdbStore.executeSql('DELETE FROM molecules WHERE id = ?', [id]);
  }

  private parseMolecule(resultSet: relationalStore.ResultSet): Molecule {
    return {
      id: resultSet.getString(resultSet.getColumnIndex('id')),
      name: resultSet.getString(resultSet.getColumnIndex('name')),
      formula: resultSet.getString(resultSet.getColumnIndex('formula')),
      molecularWeight: resultSet.getDouble(resultSet.getColumnIndex('molecular_weight')),
      smiles: resultSet.getString(resultSet.getColumnIndex('smiles')),
      description: resultSet.getString(resultSet.getColumnIndex('description')),
      category: resultSet.getString(resultSet.getColumnIndex('category')),
      tags: JSON.parse(resultSet.getString(resultSet.getColumnIndex('tags')) || '[]'),
      atoms: JSON.parse(resultSet.getString(resultSet.getColumnIndex('atoms_json')) || '[]'),
      bonds: JSON.parse(resultSet.getString(resultSet.getColumnIndex('bonds_json')) || '[]'),
      properties: JSON.parse(resultSet.getString(resultSet.getColumnIndex('properties_json')) || '[]'),
      createdAt: resultSet.getLong(resultSet.getColumnIndex('created_at')),
      updatedAt: resultSet.getLong(resultSet.getColumnIndex('updated_at'))
    };
  }
}

3. 分子编辑器:创建新分子

用户可以创建和编辑分子。

ArkTS分子编辑器:

@Component
struct MoleculeEditor {
  @State molecule: Molecule = {
    id: '',
    name: '',
    formula: '',
    molecularWeight: 0,
    smiles: '',
    description: '',
    category: '有机物',
    tags: [],
    atoms: [],
    bonds: [],
    properties: [],
    createdAt: Date.now(),
    updatedAt: Date.now()
  };

  @State selectedAtom: string = '';
  @State isAddingBond: boolean = false;
  @State bondStartAtom: string = '';

  build() {
    Column() {
      // 基本信息
        TextInput({ placeholder: '分子名称', text: this.molecule.name })
        .onChange((value: string) => {
          this.molecule.name = value;
        })
        .width('90%')
        .margin({ top: 20 })

      TextInput({ placeholder: '分子式 (如 H₂O)', text: this.molecule.formula })
        .onChange((value: string) => {
          this.molecule.formula = value;
        })
        .width('90%')
        .margin({ top: 8 })

      // 3D预览
        MoleculeRenderer({ molecule: this.molecule })
        .height(250)
        .margin({ top: 16 })

      // 原子列表
        Text('原子')
        .fontSize(16)
        .fontWeight(FontWeight.Medium)
        .alignSelf(ItemAlign.Start)
        .margin({ left: 20, top: 16 })

      List({ space: 4 }) {
        ForEach(this.molecule.atoms, (atom: Atom) => {
          ListItem() {
            Row() {
              Text(atom.element)
                .fontSize(16)
                .fontWeight(FontWeight.Bold)
                .width(40)

              Text(`(${atom.position.x.toFixed(1)}, ${atom.position.y.toFixed(1)}, ${atom.position.z.toFixed(1)})`)
                .fontSize(14)
                .fontColor('#666')

              Button('删除')
                .fontSize(12)
                .margin({ left: 'auto' })
                .onClick(() => this.removeAtom(atom.id))
            }
            .padding(8)
          }
        })
      }
      .height(150)

      // 添加原子按钮
        Row() {
        ForEach(['H', 'C', 'N', 'O', 'S', 'P'], (element: string) => {
          Button(element)
            .fontSize(14)
            .width(50)
            .margin(4)
            .onClick(() => this.addAtom(element))
        })
      }
      .margin({ top: 8 })

      // 操作按钮
        Row() {
        Button('保存')
          .layoutWeight(1)
          .onClick(() => this.saveMolecule())

        Button('分享')
          .layoutWeight(1)
          .margin({ left: 12 })
          .onClick(() => this.shareMolecule())
      }
      .width('90%')
      .margin({ top: 16, bottom: 20 })
    }
  }

  addAtom(element: string) {
    const info = periodicTable[element];
    if (!info) return;

    const newAtom: Atom = {
      id: `atom_${Date.now()}`,
      element: element,
      atomicNumber: info.atomicNumber,
      position: {
        x: Math.random() * 2 - 1,
        y: Math.random() * 2 - 1,
        z: Math.random() * 2 - 1
      },
      charge: 0,
      hybridization: 'sp3'
    };

    this.molecule.atoms.push(newAtom);
    this.molecule.molecularWeight += info.mass;
  }

  removeAtom(atomId: string) {
    const atom = this.molecule.atoms.find(a => a.id === atomId);
    if (atom) {
      const info = periodicTable[atom.element];
      if (info) {
        this.molecule.molecularWeight -= info.mass;
      }
    }

    this.molecule.atoms = this.molecule.atoms.filter(a => a.id !== atomId);
    this.molecule.bonds = this.molecule.bonds.filter(
      b => b.atom1Id !== atomId && b.atom2Id !== atomId
    );
  }

  async saveMolecule() {
    if (!this.molecule.name) {
      promptAction.showToast({ message: '请输入分子名称' });
      return;
    }

    if (this.molecule.atoms.length === 0) {
      promptAction.showToast({ message: '请添加原子' });
      return;
    }

    if (!this.molecule.id) {
      this.molecule.id = `mol_${Date.now()}`;
    }

    const db = new MoleculeDatabase();
    await db.init(getContext());
    await db.saveMolecule(this.molecule);

    promptAction.showToast({ message: '保存成功' });
    router.back();
  }

  async shareMolecule() {
    // 导出为JSON
    const jsonStr = JSON.stringify(this.molecule, null, 2);
    const filePath = getContext().cacheDir + `/${this.molecule.name || 'molecule'}.json`;

    const file = await fs.open(filePath, fs.OpenMode.CREATE | fs.OpenMode.WRITE_ONLY);
    const encoder = new util.TextEncoder();
    await fs.write(file.fd, encoder.encodeInto(jsonStr));
    await fs.close(file);

    const shareData: share.SharedData = new share.SharedData({
      uri: `file://${filePath}`,
      title: this.molecule.name
    });

    await share.show(getContext(), shareData);
  }
}

4. 化学计算器:分子量计算

分构App包含化学计算器功能。

ArkTS化学计算器:

@Component
struct ChemistryCalculator {
  @State formula: string = '';
  @State result: CalculationResult | null = null;

  build() {
    Column() {
      Text('分子量计算器')
        .fontSize(20)
        .fontWeight(FontWeight.Bold)
        .margin({ top: 20 })

      TextInput({ placeholder: '输入分子式 (如 H2O, C6H12O6)', text: this.formula })
        .onChange((value: string) => {
          this.formula = value;
          this.calculate();
        })
        .width('90%')
        .margin({ top: 16 })

      if (this.result) {
        Column() {
          Text('计算结果')
            .fontSize(16)
            .fontWeight(FontWeight.Medium)
            .margin({ top: 20 })

          this.ResultRow('分子量', `${this.result.molecularWeight.toFixed(3)} g/mol`)

          Text('元素组成')
            .fontSize(14)
            .fontColor('#666')
            .alignSelf(ItemAlign.Start)
            .margin({ left: 20, top: 12 })

          ForEach(Object.entries(this.result.composition), ([element, info]: [string, any]) => {
            Row() {
              Text(element)
                .fontSize(14)
                .fontWeight(FontWeight.Bold)
                .width(40)

              Text(`${info.count} 个原子`)
                .fontSize(14)
                .fontColor('#666')

              Text(`${info.percentage.toFixed(1)}%`)
                .fontSize(14)
                .margin({ left: 'auto' })
            }
            .padding({ left: 20, right: 20, top: 4 })
          })
        }
        .backgroundColor('#fff')
        .borderRadius(12)
        .padding(16)
        .margin({ top: 16 })
      }
    }
  }

  @Builder
  ResultRow(label: string, value: string) {
    Row() {
      Text(label)
        .fontSize(14)
        .fontColor('#666')

      Text(value)
        .fontSize(16)
        .fontWeight(FontWeight.Medium)
        .margin({ left: 'auto' })
    }
    .padding({ left: 20, right: 20, top: 8 })
  }

  calculate() {
    try {
      const parsed = this.parseFormula(this.formula);
      let totalWeight = 0;
      const composition: Record<string, { count: number; weight: number; percentage: number }> = {};

      for (const [element, count] of Object.entries(parsed)) {
        const info = periodicTable[element];
        if (!info) {
          this.result = null;
          return;
        }

        const weight = info.mass * count;
        totalWeight += weight;
        composition[element] = { count, weight, percentage: 0 };
      }

      // 计算百分比
      for (const element of Object.keys(composition)) {
        composition[element].percentage = (composition[element].weight / totalWeight) * 100;
      }

      this.result = {
        molecularWeight: totalWeight,
        composition
      };
    } catch (e) {
      this.result = null;
    }
  }

  parseFormula(formula: string): Record<string, number> {
    const result: Record<string, number> = {};
    const regex = /([A-Z][a-z]?)(\d*)/g;
    let match;

    while ((match = regex.exec(formula)) !== null) {
      if (match[1]) {
        const element = match[1];
        const count = match[2] ? parseInt(match[2]) : 1;
        result[element] = (result[element] || 0) + count;
      }
    }

    return result;
  }
}

interface CalculationResult {
  molecularWeight: number;
  composition: Record<string, { count: number; weight: number; percentage: number }>;
}

5. 数据导入导出:化学格式支持

支持导入导出常见的化学文件格式。

ArkTS格式转换:

class MoleculeFormatConverter {
  // 导出为JSON
  static toJSON(molecule: Molecule): string {
    return JSON.stringify(molecule, null, 2);
  }

  // 从JSON导入
  static fromJSON(jsonStr: string): Molecule {
    return JSON.parse(jsonStr);
  }

  // 导出为XYZ格式(常见化学格式)
  static toXYZ(molecule: Molecule): string {
    const lines = [
      molecule.atoms.length.toString(),
      molecule.name,
      ...molecule.atoms.map(atom =>
        `${atom.element} ${atom.position.x.toFixed(6)} ${atom.position.y.toFixed(6)} ${atom.position.z.toFixed(6)}`
      )
    ];
    return lines.join('\n');
  }

  // 从XYZ格式导入
  static fromXYZ(xyzStr: string): Partial<Molecule> {
    const lines = xyzStr.trim().split('\n');
    const atomCount = parseInt(lines[0]);
    const name = lines[1];

    const atoms: Atom[] = [];
    for (let i = 2; i < 2 + atomCount; i++) {
      const parts = lines[i].trim().split(/\s+/);
      const element = parts[0];
      const info = periodicTable[element];

      atoms.push({
        id: `atom_${i - 2}`,
        element,
        atomicNumber: info?.atomicNumber || 0,
        position: {
          x: parseFloat(parts[1]),
          y: parseFloat(parts[2]),
          z: parseFloat(parts[3])
        },
        charge: 0,
        hybridization: ''
      });
    }

    return {
      name,
      atoms
    };
  }

  // 导出为MOL格式
  static toMOL(molecule: Molecule): string {
    let mol = '';
    mol += molecule.name + '\n';
    mol += '  Generated by FenGou App\n';
    mol += '\n';

    // 原子数和化学键数
    mol += `${molecule.atoms.length.toString().padStart(3)}${molecule.bonds.length.toString().padStart(3)}  0  0  0  0  0  0  0  0999 V2000\n`;

    // 原子块
    molecule.atoms.forEach(atom => {
      mol += `${atom.position.x.toFixed(4).padStart(10)}${atom.position.y.toFixed(4).padStart(10)}${atom.position.z.toFixed(4).padStart(10)} ${atom.element.padEnd(3)} 0  0  0  0  0  0  0  0  0  0  0  0\n`;
    });

    // 化学键块
    molecule.bonds.forEach(bond => {
      const atom1Index = molecule.atoms.findIndex(a => a.id === bond.atom1Id) + 1;
      const atom2Index = molecule.atoms.findIndex(a => a.id === bond.atom2Id) + 1;
      const bondType = bond.type === 'single' ? 1 : bond.type === 'double' ? 2 : 3;
      mol += `${atom1Index.toString().padStart(3)}${atom2Index.toString().padStart(3)}${bondType.toString().padStart(3)}  0  0  0  0\n`;
    });

    mol += 'M  END\n';
    return mol;
  }
}

总结

分构App的分子数据管理功能,从数据模型设计、数据库存储、分子编辑器到化学计算器和格式转换,每一部分都有化学领域的特殊需求。鸿蒙端的RDB提供了可靠的数据存储,JSON处理也很方便。

如果你对化学信息学感兴趣,这些数据结构和算法都很有参考价值。分子建模、化学计算、数据可视化,这些技术在科研和教育领域都有广泛应用。

分构App就聊到这里。下一篇文章,我们换个方向,聊聊疾阅App的文字动画功能。