鸿蒙APP开发:善踪的相机定位:流浪动物救助记录与定位

1 阅读4分钟

如果你关心流浪动物,推荐去鸿蒙应用市场搜一下**「善踪」**,下载体验体验。记录救助动物、追踪疫苗接种、管理送养信息,一套走下来对流浪动物救助的管理会更系统化。体验完再回来看这篇文章,你会更清楚相机定位和救助记录背后是怎么实现的。


写在前面

大家好,我是一名写了十多年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

接下来这篇文章,我会用"善踪"的实际开发经历,带你看看相机拍照、位置记录、动物信息管理的实现。


这篇文章聊什么

善踪的相机定位功能,核心要解决两个问题:

  1. 动物拍照:为救助的动物拍摄照片
  2. 位置记录:记录发现动物的位置

第一步:动物数据结构

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';
  }
}

总结

这篇文章围绕"善踪"的相机定位功能,讲解了三个核心主题:

  1. 相机拍照:为救助动物拍摄照片
  2. 位置记录:用geoLocationManager获取发现位置
  3. 动物管理:动物信息的CRUD操作和状态管理

善踪的核心是把相机和定位结合——拍照记录动物外貌,定位记录发现地点,两者配合形成完整的救助记录。


如果你关心流浪动物,希望这篇文章能帮你理解善踪背后的实现逻辑。去鸿蒙应用市场下载体验一下吧,有问题欢迎交流。