如果你关心流浪动物,推荐去鸿蒙应用市场搜一下**「善踪」**,下载体验体验。记录救助动物、追踪疫苗接种、管理送养信息,一套走下来对流浪动物救助的管理会更系统化。体验完再回来看这篇文章,你会更清楚相机定位和救助记录背后是怎么实现的。
写在前面
大家好,我是一名写了十多年Web前端的老兵。从jQuery时代一路走到React/Vue,CSS3动画、requestAnimationFrame、Web Animation API这些都算是看家本领。去年开始转战鸿蒙生态,用ArkTS开发App,这一路踩了不少坑,也积累了不少心得。
很多人觉得"前端转鸿蒙"应该很容易——都是写UI嘛,组件化、状态管理、生命周期,概念都差不多。但真正上手之后你会发现,相似的地方让你觉得亲切,不同的地方让你抓狂。
比如:
- 相机调用:Web用
getUserMedia,鸿蒙用@ohos.camera。 - 定位服务:Web用
navigator.geolocation,鸿蒙用@ohos.geoLocationManager。 - 数据存储:Web的
localStorage到了ArkTS变成了@ohos.data.preferences。
接下来这篇文章,我会用"善踪"的实际开发经历,带你看看相机拍照、位置记录、动物信息管理的实现。
这篇文章聊什么
善踪的相机定位功能,核心要解决两个问题:
- 动物拍照:为救助的动物拍摄照片
- 位置记录:记录发现动物的位置
第一步:动物数据结构
interface Animal {
id: string;
name: string;
type: string; // 狗/猫/兔/鸟/鼠/其他
status: string; // 已救助/寄养中/已送养/治疗中/已离世
health: string; // 健康/一般/虚弱/重症
foundDate: string;
foundLocation: string;
foundLatitude: number;
foundLongitude: number;
photoUri: string;
description: string;
vaccines: VaccineRecord[];
adoption: AdoptionRecord | null;
visits: VisitRecord[];
isFavorite: boolean;
}
interface VaccineRecord {
id: string;
name: string;
date: string;
nextDate: string;
notes: string;
}
interface AdoptionRecord {
adopterName: string;
adopterPhone: string;
adoptDate: string;
notes: string;
}
interface VisitRecord {
date: string;
condition: string;
notes: string;
}
const ANIMAL_TYPES = [
{ id: 'dog', name: '狗', icon: '🐶' },
{ id: 'cat', name: '猫', icon: '🐱' },
{ id: 'rabbit', name: '兔', icon: '🐰' },
{ id: 'bird', name: '鸟', icon: '🐦' },
{ id: 'hamster', name: '鼠', icon: '🐹' },
{ id: 'other', name: '其他', icon: '🐾' }
];
const ANIMAL_STATUS = [
{ id: 'rescued', name: '已救助', color: '#10B981' },
{ id: 'fostering', name: '寄养中', color: '#3B82F6' },
{ id: 'adopted', name: '已送养', color: '#F59E0B' },
{ id: 'treating', name: '治疗中', color: '#EF4444' },
{ id: 'deceased', name: '已离世', color: '#6B7280' }
];
第二步:添加动物记录
@Entry
@Component
struct AddAnimalPage {
@State name: string = ''
@State type: string = 'cat'
@State description: string = ''
@State foundLocation: string = ''
@State latitude: number = 0
@State longitude: number = 0
@State photoUri: string = ''
build() {
Column() {
// 动物类型选择
Flex({ wrap: FlexWrap.Wrap }) {
ForEach(ANIMAL_TYPES, (animalType) => {
Column() {
Text(animalType.icon)
.fontSize(24)
Text(animalType.name)
.fontSize(12)
}
.padding(12)
.borderRadius(12)
.backgroundColor(this.type === animalType.id ? '#10B981' : '#374151')
.onClick(() => { this.type = animalType.id })
})
}
.margin({ bottom: 16 })
// 名字
TextInput({ placeholder: '给它起个名字', text: this.name })
.onChange((v: string) => { this.name = v })
.margin({ bottom: 12 })
// 描述
TextInput({ placeholder: '描述一下它的状况', text: this.description })
.onChange((v: string) => { this.description = v })
.margin({ bottom: 12 })
// 位置
Row() {
TextInput({ placeholder: '发现地点', text: this.foundLocation })
.onChange((v: string) => { this.foundLocation = v })
.layoutWeight(1)
Button('定位')
.onClick(() => this.getCurrentLocation())
.height(40)
.margin({ left: 8 })
.backgroundColor('#3B82F6')
}
.width('100%')
.margin({ bottom: 12 })
if (this.latitude !== 0) {
Text(`坐标:${this.latitude.toFixed(6)}, ${this.longitude.toFixed(6)}`)
.fontSize(12)
.fontColor('#9CA3AF')
.margin({ bottom: 12 })
}
// 拍照
Button('拍照')
.onClick(() => this.takePhoto())
.width('100%')
.height(48)
.backgroundColor('#F59E0B')
.margin({ bottom: 16 })
Button('保存记录')
.onClick(() => this.saveAnimal())
.width('100%')
.backgroundColor('#10B981')
}
.width('100%')
.height('100%')
.padding(16)
.backgroundColor('#111827')
}
async getCurrentLocation() {
try {
const atManager = abilityAccessCtrl.createAtManager();
const result = await atManager.requestPermissionsFromUser(
getContext(),
['ohos.permission.LOCATION']
);
if (result.authResults[0] === 0) {
geoLocationManager.getCurrentLocation((err, location) => {
if (!err) {
this.latitude = location.latitude;
this.longitude = location.longitude;
}
});
}
} catch (err) {
console.error(`获取位置失败: ${err}`);
}
}
takePhoto() {
// 相机拍照逻辑
}
async saveAnimal() {
const animal: Animal = {
id: `animal_${Date.now()}`,
name: this.name,
type: this.type,
status: 'rescued',
health: '一般',
foundDate: new Date().toISOString().slice(0, 10),
foundLocation: this.foundLocation,
foundLatitude: this.latitude,
foundLongitude: this.longitude,
photoUri: this.photoUri,
description: this.description,
vaccines: [],
adoption: null,
visits: [],
isFavorite: false
};
const store = await preferences.getPreferences(getContext(), 'shanzong_data');
let animals: Animal[] = JSON.parse(await store.get('animals', '[]') as string);
animals.push(animal);
await store.set('animals', JSON.stringify(animals));
await store.flush();
router.back();
}
}
第三步:动物列表页面
@Entry
@Component
struct AnimalListPage {
@State animals: Animal[] = []
@State filterStatus: string = 'all'
get filteredAnimals(): Animal[] {
if (this.filterStatus === 'all') return this.animals;
return this.animals.filter(a => a.status === this.filterStatus);
}
build() {
Column() {
// 状态筛选
Flex({ wrap: FlexWrap.Wrap }) {
Text('全部')
.fontSize(12)
.padding(6)
.margin(4)
.borderRadius(6)
.backgroundColor(this.filterStatus === 'all' ? '#10B981' : '#374151')
.onClick(() => { this.filterStatus = 'all' })
ForEach(ANIMAL_STATUS, (status) => {
Text(status.name)
.fontSize(12)
.padding(6)
.margin(4)
.borderRadius(6)
.backgroundColor(this.filterStatus === status.id ? status.color : '#374151')
.onClick(() => { this.filterStatus = status.id })
})
}
.margin({ bottom: 16 })
// 动物列表
List({ space: 12 }) {
ForEach(this.filteredAnimals, (animal: Animal) => {
ListItem() {
Row() {
if (animal.photoUri) {
Image(animal.photoUri)
.width(60)
.height(60)
.objectFit(ImageFit.Cover)
.borderRadius(8)
}
Column() {
Text(`${this.getAnimalIcon(animal.type)} ${animal.name}`)
.fontSize(16)
.fontWeight(FontWeight.Bold)
Text(this.getStatusName(animal.status))
.fontSize(13)
.fontColor(this.getStatusColor(animal.status))
.margin({ top: 4 })
}
.layoutWeight(1)
.margin({ left: 12 })
}
.width('100%')
.padding(12)
.backgroundColor('#1F2937')
.borderRadius(12)
}
})
}
.layoutWeight(1)
}
.width('100%')
.height('100%')
.padding(16)
.backgroundColor('#111827')
}
private getAnimalIcon(type: string): string {
return ANIMAL_TYPES.find(t => t.id === type)?.icon || '🐾';
}
private getStatusName(status: string): string {
return ANIMAL_STATUS.find(s => s.id === status)?.name || status;
}
private getStatusColor(status: string): string {
return ANIMAL_STATUS.find(s => s.id === status)?.color || '#6B7280';
}
}
总结
这篇文章围绕"善踪"的相机定位功能,讲解了三个核心主题:
- 相机拍照:为救助动物拍摄照片
- 位置记录:用
geoLocationManager获取发现位置 - 动物管理:动物信息的CRUD操作和状态管理
善踪的核心是把相机和定位结合——拍照记录动物外貌,定位记录发现地点,两者配合形成完整的救助记录。
如果你关心流浪动物,希望这篇文章能帮你理解善踪背后的实现逻辑。去鸿蒙应用市场下载体验一下吧,有问题欢迎交流。