分构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的文字动画功能。