第10篇:农事记录与操作管理

教程目标

通过本篇教程,你将学会:

  • 理解农事操作数据模型设计
  • 实现农事记录的增删改查
  • 创建农事记录列表页面
  • 创建添加农事记录页面
  • 集成天气服务获取智能农事建议
  • 实现农事记录编辑功能
  • 管理农事记录中的图片和成本信息

完成本教程后,你将拥有完整的农事记录管理功能。


一、农事操作数据模型

在实现农事记录管理功能之前,我们需要了解农事操作数据模型的结构。农事操作模型已在 ProfessionalAgricultureModels.ets 中定义。

1.1 查看农事操作数据模型

文件位置entry/src/main/ets/models/ProfessionalAgricultureModels.ets(第163-192行)

操作说明

  1. 打开 entry/src/main/ets/models/ProfessionalAgricultureModels.ets
  2. 查看 FarmOperation 接口的定义
/**
 * 农事操作记录
 * 记录在地块上进行的各种农事操作
 */
export interface FarmOperation {
  id: string;                    // 操作记录唯一标识
  fieldId: string;               // 关联地块ID
  cropId?: string;               // 关联作物ID(可选)
  operationType: string;         // 操作类型:整地/播种/施肥/灌溉/植保/除草/收获等
  date: number;                  // 操作日期时间戳
  operator: string;              // 操作人
  details: string;               // ���作详细说明
  materials?: MaterialUsage[];   // 使用的物资(可选)
  laborHours?: number;           // 用工工时(可选)
  machineryUsed?: string[];      // 使用的机械(可选)
  cost?: number;                 // 成本(元)
  images?: ImageInfo[];          // 现场照片(可选)
  weather?: string;              // 天气情况(可选)
  notes?: string;                // 备注信息(可选)
  createdAt: number;             // 创建时间
}

/**
 * 物资使用记录
 * 记录农事操作中使用的物资信息
 */
export interface MaterialUsage {
  materialName: string;          // 物资名称
  quantity: number;              // 使用数量
  unit: string;                  // 单位
  unitPrice?: number;            // 单价(元)
  totalCost?: number;            // 总成本(元)
}

模型设计要点

设计要点 说明
时间存储 使用时间戳(number)存储日期,便于排序和计算
可选字段 使用 ? 标记非必填字段(如cropId、materials、cost)
关联关系 通过 fieldId 与地块关联,通过 cropId 与作物关联
灵活扩展 支持物资记录、用工工时、机械使用等扩展信息

1.2 数据关系说明

FieldInfo(地块信息)
    ├── operations: FarmOperation[]  // 农事操作记录数组
    ├── costRecords: CostRecord[]    // 成本记录数组
    └── salesRecords: SalesRecord[]  // 销售记录数组

农事记录作为地块的一个属性存在,这样设计的好处是:

  1. 数据关联清晰,可以直接通过地块获取所有农事记录
  2. 便于按地块进行数据统计和分析
  3. 支持离线场景下的数据管理

二、农事操作服务

农事操作的相关方法已在 FieldService 中实现。让我们查看这些方法。

2.1 FieldService 中的农事操作方法

文件位置entry/src/main/ets/services/FieldService.ets

核心方法

/**
 * 添加农事记录
 * @param fieldId 地块ID
 * @param operation 农事操作对象
 * @returns Promise<boolean> 成功返回true,失败返回false
 */
async addFarmOperation(fieldId: string, operation: FarmOperation): Promise<boolean> {
  const field = await this.getFieldById(fieldId);
  if (field) {
    // 初始化operations数组
    if (!field.operations) {
      field.operations = [];
    }
    // 添加操作记录
    field.operations.push(operation);
    // 更新地块信息
    return await this.updateField(field);
  }
  return false;
}

/**
 * 获取地块的所有农事记录
 * @param fieldId 地块ID
 * @returns Promise<FarmOperation[]> 农事记录列表
 */
async getFieldOperations(fieldId: string): Promise<FarmOperation[]> {
  const field = await this.getFieldById(fieldId);
  return field?.operations || [];
}

/**
 * 生成操作记录ID
 * @returns string 格式为 "op_时间戳_随机字符串"
 */
generateOperationId(): string {
  return 'op_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
}

/**
 * 获取最近的农事记录
 * @param limit 返回数量限制,默认10条
 * @returns Promise<FarmOperation[]> 按日期倒序排列的农事记录
 */
async getRecentOperations(limit: number = 10): Promise<FarmOperation[]> {
  const fields = await this.getAllFields();
  const allOperations: FarmOperation[] = [];

  // 遍历所有地块收集农事记录
  for (const field of fields) {
    if (field.operations && field.operations.length > 0) {
      for (const op of field.operations) {
        allOperations.push(op);
      }
    }
  }

  // 按日期排序,最新的在前
  allOperations.sort((a, b) => b.date - a.date);

  return allOperations.slice(0, limit);
}

/**
 * 记录灌溉操作(便捷方法)
 * @param fieldId 地块ID
 * @param details 操作详情
 * @returns Promise<boolean>
 */
async recordIrrigation(fieldId: string, details: string): Promise<boolean> {
  const operation: FarmOperation = {
    id: this.generateOperationId(),
    fieldId: fieldId,
    operationType: '灌溉',
    date: Date.now(),
    operator: '当前用户',
    details: details,
    createdAt: Date.now()
  };
  return await this.addFarmOperation(fieldId, operation);
}

/**
 * 记录施肥操作(便捷方法)
 * @param fieldId 地块ID
 * @param details 操作详情
 * @returns Promise<boolean>
 */
async recordFertilization(fieldId: string, details: string): Promise<boolean> {
  const operation: FarmOperation = {
    id: this.generateOperationId(),
    fieldId: fieldId,
    operationType: '施肥',
    date: Date.now(),
    operator: '当前用户',
    details: details,
    createdAt: Date.now()
  };
  return await this.addFarmOperation(fieldId, operation);
}

/**
 * 记录植保操作(便捷方法)
 * @param fieldId 地块ID
 * @param details 操作详情
 * @returns Promise<boolean>
 */
async recordPestControl(fieldId: string, details: string): Promise<boolean> {
  const operation: FarmOperation = {
    id: this.generateOperationId(),
    fieldId: fieldId,
    operationType: '植保',
    date: Date.now(),
    operator: '当前用户',
    details: details,
    createdAt: Date.now()
  };
  return await this.addFarmOperation(fieldId, operation);
}

服务设计要点

功能点 实现方式
单例模式 使用 getInstance() 确保全局唯一实例
数据持久化 通过 StorageUtil 保存到本地存储
关联管理 农事记录作为地块的子数据存储
便捷方法 提供灌溉、施肥、植保等常用操作的快捷方法

三、创建农事记录列表页面

农事记录列表页面用于展示和管理农事操作记录,支持筛选和搜索功能。

3.1 创建 FarmOperationPage.ets

文件位置entry/src/main/ets/pages/Management/FarmOperationPage.ets

操作步骤

  1. entry/src/main/ets/pages/Management/ 目录下创建新文件
  2. 命名为 FarmOperationPage.ets
  3. 输入以下代码
/**
 * 农事记录页面
 * 展示和管理农事操作记录
 * 支持按类型筛选、搜索等功能
 */

import { router } from '@kit.ArkUI';
import { promptAction } from '@kit.ArkUI';
import { FarmOperation, FieldInfo } from '../../models/ProfessionalAgricultureModels';
import { ImageInfo } from '../../models/CommonModels';
import { FieldService } from '../../services/FieldService';

@Entry
@ComponentV2
export struct FarmOperationPage {
  // 状态变量
  @Local operations: FarmOperation[] = [];           // 所有农事记录
  @Local filteredOperations: FarmOperation[] = [];    // 筛选后的记录
  @Local field: FieldInfo | null = null;             // 当前地块(如果从地块详情进入)
  @Local isLoading: boolean = true;                   // 加载状态
  @Local selectedType: string = '全部';               // 选中的操作类型
  @Local searchText: string = '';                     // 搜索文本

  // 服务实例
  private fieldService = FieldService.getInstance();
  private fieldId: string = '';                       // 地块ID
  private isFirstLoad: boolean = true;                // 是否首次加载

  // 操作类型选项
  private operationTypes = ['全部', '播种', '施肥', '浇水', '除草', '打药', '收获', '其他'];

  /**
   * 页面即将出现时加载数据
   */
  async aboutToAppear(): Promise<void> {
    // 获取传入的地块ID参数
    const params = router.getParams() as Record<string, Object>;
    if (params && params['fieldId']) {
      this.fieldId = params['fieldId'] as string;
    }
    await this.loadData();
  }

  /**
   * 页面显示时刷新数据(从其他页面返回时)
   */
  async onPageShow(): Promise<void> {
    // 首次加载时跳过,避免重复加载
    if (this.isFirstLoad) {
      this.isFirstLoad = false;
      return;
    }
    // 从其他页面返回时重新加载数据
    await this.loadData();
  }

  /**
   * 加载农事记录数据
   */
  async loadData(): Promise<void> {
    try {
      if (this.fieldId) {
        // 加载特定地块的农事记录
        this.field = await this.fieldService.getFieldById(this.fieldId);
        this.operations = await this.fieldService.getFieldOperations(this.fieldId);
      } else {
        // 加载所有地块的农事记录
        const allFields = await this.fieldService.getAllFields();
        this.operations = [];
        for (const field of allFields) {
          const ops = await this.fieldService.getFieldOperations(field.id);
          for (const op of ops) {
            this.operations.push(op);
          }
        }
      }
      this.filterOperations();
      this.isLoading = false;
    } catch (error) {
      console.error('[FarmOperationPage] Failed to load operations:', error);
      this.isLoading = false;
    }
  }

  /**
   * 筛选农事记录
   */
  filterOperations(): void {
    let filtered = this.operations;

    // 按类型筛选
    if (this.selectedType !== '全部') {
      filtered = filtered.filter(op => op.operationType === this.selectedType);
    }

    // 按搜索文本筛选
    if (this.searchText.trim().length > 0) {
      const keyword = this.searchText.toLowerCase();
      filtered = filtered.filter(op =>
        op.operationType.toLowerCase().includes(keyword) ||
        op.operator.toLowerCase().includes(keyword) ||
        (op.details && op.details.toLowerCase().includes(keyword))
      );
    }

    // 按日期排序(最新的在前)
    this.filteredOperations = filtered.sort((a, b) => b.date - a.date);
  }

  build() {
    Column() {
      this.buildHeader()

      if (this.isLoading) {
        this.buildLoading()
      } else {
        Column() {
          this.buildFilters()
          this.buildList()
        }
        .layoutWeight(1)
      }
    }
    .width('100%')
    .height('100%')
    .backgroundColor($r('app.color.background'))
  }

  /**
   * 构建顶部导航栏
   */
  @Builder
  buildHeader() {
    Row() {
      // 返回按钮
      Button('< 返回')
        .backgroundColor(Color.Transparent)
        .fontColor($r('app.color.text_primary'))
        .onClick(() => {
          router.back();
        })

      // 标题区域
      Column() {
        Text('农事记录')
          .fontSize(18)
          .fontWeight(FontWeight.Bold)
        if (this.field) {
          Text(this.field.name)
            .fontSize(12)
            .fontColor($r('app.color.text_secondary'))
            .margin({ top: 2 })
        } else {
          Text('全部地块')
            .fontSize(12)
            .fontColor($r('app.color.text_secondary'))
            .margin({ top: 2 })
        }
      }
      .layoutWeight(1)

      // 添加按钮
      Button('+ 添加')
        .backgroundColor(Color.Transparent)
        .fontColor($r('app.color.primary_professional'))
        .onClick(() => {
          if (this.fieldId) {
            // 从特定地块进入,直接添加该地块的农事记录
            router.pushUrl({
              url: 'pages/Management/AddFarmOperationPage',
              params: { fieldId: this.fieldId }
            });
          } else {
            // 从全部地块进入,提示用户先选择地块
            promptAction.showToast({
              message: '请先从地块管理中选择地块添加农事记录',
              duration: 2500
            });
          }
        })
    }
    .width('100%')
    .height(56)
    .padding({ left: 16, right: 16 })
    .backgroundColor($r('app.color.card_background'))
    .shadow({ radius: 4, color: $r('app.color.shadow_light'), offsetY: 2 })
  }

  /**
   * 构建加载状态
   */
  @Builder
  buildLoading() {
    Column() {
      Text('加载中...')
        .fontSize(16)
        .fontColor($r('app.color.text_secondary'))
    }
    .width('100%')
    .layoutWeight(1)
    .justifyContent(FlexAlign.Center)
  }

  /**
   * 构建筛选区域
   */
  @Builder
  buildFilters() {
    Column({ space: 12 }) {
      // 搜索框
      TextInput({ placeholder: '搜索操作类型、操作人或详情...' })
        .fontSize(15)
        .height(40)
        .backgroundColor($r('app.color.card_background'))
        .onChange((value: string) => {
          this.searchText = value;
          this.filterOperations();
        })

      // 类型筛选器(横向滚动)
      Scroll() {
        Row({ space: 8 }) {
          ForEach(this.operationTypes, (type: string) => {
            Text(type)
              .fontSize(14)
              .fontColor(this.selectedType === type ?
                Color.White : $r('app.color.text_primary'))
              .padding({ left: 16, right: 16, top: 8, bottom: 8 })
              .backgroundColor(this.selectedType === type ?
                $r('app.color.primary_professional') : $r('app.color.card_background'))
              .borderRadius(16)
              .onClick(() => {
                this.selectedType = type;
                this.filterOperations();
              })
          })
        }
      }
      .scrollable(ScrollDirection.Horizontal)
      .scrollBar(BarState.Off)
      .width('100%')
    }
    .width('100%')
    .padding(16)
  }

  /**
   * 构建记录列表
   */
  @Builder
  buildList() {
    if (this.filteredOperations.length === 0) {
      // 空状态
      Column({ space: 16 }) {
        Text('📝')
          .fontSize(48)
        Text(this.searchText || this.selectedType !== '全部' ? '没有找到匹配的记录' : '还没有农事记录')
          .fontSize(16)
          .fontColor($r('app.color.text_secondary'))
        Text('点击右上角添加新的农事操作')
          .fontSize(14)
          .fontColor($r('app.color.text_secondary'))
      }
      .width('100%')
      .layoutWeight(1)
      .justifyContent(FlexAlign.Center)
    } else {
      // 记录列表
      Column() {
        Text(`${this.filteredOperations.length} 条记录`)
          .fontSize(14)
          .fontColor($r('app.color.text_secondary'))
          .width('100%')
          .padding({ left: 16, right: 16, bottom: 8 })

        Scroll() {
          Column({ space: 12 }) {
            ForEach(this.filteredOperations, (operation: FarmOperation) => {
              this.buildOperationCard(operation)
            })
          }
          .padding({ left: 16, right: 16, bottom: 16 })
        }
        .layoutWeight(1)
        .scrollBar(BarState.Auto)
      }
      .layoutWeight(1)
    }
  }

  /**
   * 构建农事记录卡片
   */
  @Builder
  buildOperationCard(operation: FarmOperation) {
    Column({ space: 12 }) {
      // 顶部:图标、类型、日期
      Row() {
        Text(this.getOperationIcon(operation.operationType))
          .fontSize(20)
        Text(operation.operationType)
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .margin({ left: 8 })
        Blank()
        Text(this.formatDate(operation.date))
          .fontSize(13)
          .fontColor($r('app.color.text_secondary'))
      }
      .width('100%')

      // 详细说明
      if (operation.details) {
        Text(operation.details)
          .fontSize(14)
          .fontColor($r('app.color.text_primary'))
          .lineHeight(20)
          .width('100%')
      }

      // 底部:操作人和成本
      Row() {
        Text('操作人:' + operation.operator)
          .fontSize(12)
          .fontColor($r('app.color.text_secondary'))

        if (operation.cost !== undefined && operation.cost > 0) {
          Blank()
          Text('成本:¥' + operation.cost.toFixed(2))
            .fontSize(12)
            .fontColor('#FF6B6B')
        }
      }
      .width('100%')

      // 照片展示
      if (operation.images && operation.images.length > 0) {
        Grid() {
          ForEach(operation.images.slice(0, 3), (img: ImageInfo) => {
            GridItem() {
              Image('file://' + img.uri)
                .width('100%')
                .height(80)
                .objectFit(ImageFit.Cover)
                .borderRadius(8)
            }
          })
        }
        .columnsTemplate('1fr 1fr 1fr')
        .columnsGap(8)
        .width('100%')
      }
    }
    .width('100%')
    .padding(16)
    .backgroundColor($r('app.color.card_background'))
    .borderRadius(12)
    .shadow({ radius: 4, color: $r('app.color.shadow_light'), offsetY: 2 })
    .onClick(() => {
      // 点击卡片跳转到编辑页面
      router.pushUrl({
        url: 'pages/Management/EditFarmOperationPage',
        params: {
          fieldId: operation.fieldId,
          operationId: operation.id
        }
      });
    })
  }

  /**
   * 获取操作类型对应的图标
   */
  private getOperationIcon(type: string): string {
    const iconMap: Record<string, string> = {
      '播种': '🌱',
      '施肥': '🌿',
      '浇水': '💧',
      '除草': '🌾',
      '打药': '🛡️',
      '收获': '🌻',
      '其他': '📋'
    };
    return iconMap[type] || '📝';
  }

  /**
   * 格式化日期时间
   */
  private formatDate(timestamp: number): string {
    const date = new Date(timestamp);
    const year = date.getFullYear();
    const month = (date.getMonth() + 1).toString().padStart(2, '0');
    const day = date.getDate().toString().padStart(2, '0');
    const hours = date.getHours().toString().padStart(2, '0');
    const minutes = date.getMinutes().toString().padStart(2, '0');
    return `${year}-${month}-${day} ${hours}:${minutes}`;
  }
}

页面功能说明

功能 实现方式
数据加载 支持加载单个地块或所有地块的农事记录
类型筛选 横向滚动的标签选择器
搜索功能 按操作类型、操作人、详情进行模糊搜索
卡片展示 显示操作类型、日期、详情、操作人、成本、照片
空状态 无数据时显示友好提示

四、创建添加农事记录页面

添加农事记录页面允许用户记录各种农事操作,并集成天气服务提供智能建议。

4.1 操作类型定义

首先定义操作类型的数据结构:

/**
 * 操作类型模板接口
 */
interface OperationType {
  type: string;        // 操作类型名称
  icon: string;        // 操作图标
  template: string;    // 操作说明模板
}

/**
 * 天气对农事操作的影响
 */
interface WeatherAdvice {
  unsuitable: string[];  // 不宜进行的操作
  suitable: string[];    // 适宜进行的操作
  tips: string;          // 提示信息
}

4.2 创建 AddFarmOperationPage.ets

文件位置entry/src/main/ets/pages/Management/AddFarmOperationPage.ets

操作步骤

  1. entry/src/main/ets/pages/Management/ 目录下创建新文件
  2. 命名为 AddFarmOperationPage.ets
  3. 输入以下代码
/**
 * 添加农事记录页面
 * 支持记录农事操作,集成天气服务提供智能建议
 */

import { router } from '@kit.ArkUI';
import { promptAction } from '@kit.ArkUI';
import { FarmOperation } from '../../models/ProfessionalAgricultureModels';
import { ImageInfo } from '../../models/CommonModels';
import { FieldService } from '../../services/FieldService';
import { ImageService, ImagePickResult } from '../../services/ImageService';
import { common } from '@kit.AbilityKit';
import { WeatherSearch, WeatherSearchQuery, LocalWeatherLive, LocalWeatherLiveResult, OnWeatherSearchListener } from '@amap/amap_lbs_search';

// 操作类型模板接口
interface OperationType {
  type: string;
  icon: string;
  template: string;
}

// 天气对农事操作的影响
interface WeatherAdvice {
  unsuitable: string[];  // 不宜进行的操作
  suitable: string[];    // 适宜进行的操作
  tips: string;          // 提示信息
}

@Entry
@ComponentV2
struct AddFarmOperationPage {
  // ===== 状态变量 =====
  @Local selectedType: string = '';          // 选中的操作类型
  @Local selectedDate: number = Date.now();  // 操作日期
  @Local operator: string = '当前用户';      // 操作人
  @Local details: string = '';               // 详细说明
  @Local cost: string = '';                  // 成本
  @Local selectedImages: ImageInfo[] = [];   // 选择的图片
  @Local isSaving: boolean = false;          // 保存中状态

  // 天气相关状态
  @Local currentWeather: string = '';        // 当前天气
  @Local currentTemperature: string = '';    // 当前温度
  @Local windDirection: string = '';         // 风向
  @Local windPower: string = '';             // 风力
  @Local humidity: string = '';              // 湿度
  @Local weatherLoading: boolean = true;     // 天气加载状态
  @Local weatherAdvice: WeatherAdvice = { unsuitable: [], suitable: [], tips: '' };

  // ===== 服务实例 =====
  private fieldService = FieldService.getInstance();
  private imageService = ImageService.getInstance();
  private fieldId: string = '';

  // ===== 操作类型模板配置 =====
  private operationTypes: OperationType[] = [
    { type: '播种', icon: '🌱', template: '进行播种作业' },
    { type: '施肥', icon: '🌿', template: '进行施肥作业' },
    { type: '浇水', icon: '💧', template: '进行浇水作业' },
    { type: '除草', icon: '🌾', template: '进行除草作业' },
    { type: '打药', icon: '🛡️', template: '进行植保打药作业' },
    { type: '收获', icon: '🌻', template: '进行收获作业' },
    { type: '整地', icon: '🚜', template: '进行整地作业' },
    { type: '修剪', icon: '✂️', template: '进行修剪作业' },
    { type: '其他', icon: '📋', template: '' }
  ];

  /**
   * 页面初始化
   */
  aboutToAppear(): void {
    // 获取传入的地块ID
    const params = router.getParams() as Record<string, Object>;
    if (params && params['fieldId']) {
      this.fieldId = params['fieldId'] as string;
    }

    // 初始化ImageService
    const context = getContext(this) as common.UIAbilityContext;
    this.imageService.initialize(context);

    // 获取天气信息
    this.loadWeatherInfo(context);
  }

  /**
   * 加载天气信息
   */
  private loadWeatherInfo(context: Context): void {
    try {
      // 从全局存储获取城市信息
      const globalLocation = AppStorage.get<Record<string, string>>('globalUserLocation');
      const city = globalLocation?.city || '武汉';

      const weatherSearch = new WeatherSearch(context);
      const query = new WeatherSearchQuery(city, 1); // 1表示实时天气

      const listener: OnWeatherSearchListener = {
        onWeatherLiveSearched: (result: LocalWeatherLiveResult | undefined, rCode: number) => {
          if (rCode === 1000 && result) {
            const liveWeather: LocalWeatherLive = result.getLiveResult();
            if (liveWeather) {
              this.currentWeather = liveWeather.getWeather() || '未知';
              this.currentTemperature = liveWeather.getTemperature() || '--';
              this.windDirection = liveWeather.getWindDirection() || '';
              this.windPower = liveWeather.getWindPower() || '';
              this.humidity = liveWeather.getHumidity() || '';

              // 根据天气生成农事建议
              this.generateWeatherAdvice();
            }
          }
          this.weatherLoading = false;
        },
        onWeatherForecastSearched: () => {}
      };

      weatherSearch.setOnWeatherSearchListener(listener);
      weatherSearch.setQuery(query);
      weatherSearch.searchWeatherAsyn();
    } catch (error) {
      console.error('[AddFarmOperationPage] Failed to load weather:', error);
      this.weatherLoading = false;
      this.weatherAdvice = {
        unsuitable: [],
        suitable: ['播种', '施肥', '浇水', '除草', '打药', '收获', '整地', '修剪'],
        tips: '天气信息获取失败,请根据实际情况安排农事'
      };
    }
  }

  /**
   * 根据天气生成农事建议
   */
  private generateWeatherAdvice(): void {
    const weather = this.currentWeather.toLowerCase();
    const temp = parseInt(this.currentTemperature) || 20;
    const windPower = parseInt(this.windPower) || 0;

    let unsuitable: string[] = [];
    let suitable: string[] = [];
    let tips = '';

    // 雨天
    if (weather.includes('雨') || weather.includes('雷') || weather.includes('暴')) {
      unsuitable = ['播种', '施肥', '打药', '收获', '整地'];
      suitable = ['其他'];
      tips = '⚠️ 雨天不宜进行户外农事作业,施肥打药易被雨水冲刷,收获作物易受潮';
    }
    // 大风天
    else if (windPower >= 5) {
      unsuitable = ['打药', '施肥'];
      suitable = ['浇水', '除草', '修剪', '其他'];
      tips = '⚠️ 大风天气不宜打药施肥,药剂易飘散造成浪费和污染';
    }
    // 高温天气
    else if (temp >= 35) {
      unsuitable = ['打药', '施肥', '整地'];
      suitable = ['浇水', '收获'];
      tips = '⚠️ 高温天气避免中午作业,打药易产生药害,建议早晚进行';
    }
    // 低温天气
    else if (temp <= 5) {
      unsuitable = ['播种', '打药'];
      suitable = ['整地', '修剪'];
      tips = '⚠️ 低温天气不宜播种,药剂效果差,可进行整地修剪等准备工作';
    }
    // 阴天
    else if (weather.includes('阴') || weather.includes('多云')) {
      unsuitable = [];
      suitable = ['播种', '施肥', '浇水', '除草', '打药', '收获', '整地', '修剪'];
      tips = '✅ 阴天适宜大多数农事作业,光照柔和,作业舒适';
    }
    // 晴天
    else if (weather.includes('晴')) {
      if (temp >= 30) {
        unsuitable = ['打药'];
        suitable = ['播种', '施肥', '浇水', '除草', '收获', '整地', '修剪'];
        tips = '☀️ 晴天高温,打药建议在早晚进行,避免药害';
      } else {
        unsuitable = [];
        suitable = ['播种', '施肥', '浇水', '除草', '打药', '收获', '整地', '修剪'];
        tips = '✅ 天气晴好,适宜各类农事作业';
      }
    }
    // 默认
    else {
      unsuitable = [];
      suitable = ['播种', '施肥', '浇水', '除草', '打药', '收获', '整地', '修剪'];
      tips = '请根据实际天气情况合理安排农事作业';
    }

    this.weatherAdvice = { unsuitable, suitable, tips };
  }

  /**
   * 检查操作是否不宜进行
   */
  private isUnsuitableOperation(type: string): boolean {
    return this.weatherAdvice.unsuitable.includes(type);
  }

  build() {
    Column() {
      this.buildHeader()

      Scroll() {
        Column({ space: 16 }) {
          this.buildWeatherTip()
          this.buildOperationType()
          this.buildBasicInfo()
          this.buildImages()
        }
        .padding({ left: 16, right: 16, top: 16, bottom: 80 })
      }
      .layoutWeight(1)
      .scrollBar(BarState.Auto)

      this.buildFooter()
    }
    .width('100%')
    .height('100%')
    .backgroundColor($r('app.color.background'))
  }

  /**
   * 构建顶部导航栏
   */
  @Builder
  buildHeader() {
    Row() {
      Button('< 取消')
        .backgroundColor(Color.Transparent)
        .fontColor($r('app.color.text_primary'))
        .onClick(() => {
          router.back();
        })

      Text('添加农事记录')
        .fontSize(18)
        .fontWeight(FontWeight.Bold)
        .layoutWeight(1)
        .textAlign(TextAlign.Center)

      Text('     ')
        .width(60)
    }
    .width('100%')
    .height(56)
    .padding({ left: 16, right: 16 })
    .backgroundColor($r('app.color.card_background'))
    .shadow({ radius: 4, color: $r('app.color.shadow_light'), offsetY: 2 })
  }

  /**
   * 构建天气提示卡片
   */
  @Builder
  buildWeatherTip() {
    Column({ space: 10 }) {
      // 天气信息头部
      Row({ space: 8 }) {
        Text('🌤️')
          .fontSize(20)
        Text('当前天气')
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .fontColor($r('app.color.text_primary'))
        Blank()
        if (this.weatherLoading) {
          LoadingProgress()
            .width(20)
            .height(20)
            .color($r('app.color.primary_professional'))
        }
      }
      .width('100%')

      if (!this.weatherLoading && this.currentWeather) {
        // 天气详情
        Row({ space: 16 }) {
          // 天气和温度
          Column({ space: 4 }) {
            Text(this.currentWeather)
              .fontSize(18)
              .fontWeight(FontWeight.Bold)
              .fontColor($r('app.color.text_primary'))
            Text(`${this.currentTemperature}°C`)
              .fontSize(24)
              .fontWeight(FontWeight.Bold)
              .fontColor($r('app.color.primary_professional'))
          }
          .alignItems(HorizontalAlign.Start)

          // 分隔线
          Divider()
            .vertical(true)
            .height(50)
            .color($r('app.color.border'))

          // 风力湿度
          Column({ space: 4 }) {
            Row({ space: 4 }) {
              Text('💨')
                .fontSize(14)
              Text(`${this.windDirection}${this.windPower}`)
                .fontSize(13)
                .fontColor($r('app.color.text_secondary'))
            }
            Row({ space: 4 }) {
              Text('💧')
                .fontSize(14)
              Text(`湿度 ${this.humidity}%`)
                .fontSize(13)
                .fontColor($r('app.color.text_secondary'))
            }
          }
          .alignItems(HorizontalAlign.Start)
        }
        .width('100%')
        .padding({ top: 8, bottom: 8 })

        // 农事建议
        if (this.weatherAdvice.tips) {
          Column({ space: 8 }) {
            Text(this.weatherAdvice.tips)
              .fontSize(14)
              .fontColor(this.weatherAdvice.unsuitable.length > 0 ? '#E65100' : '#2E7D32')
              .width('100%')

            // 不宜操作提示
            if (this.weatherAdvice.unsuitable.length > 0) {
              Row({ space: 6 }) {
                Text('不宜:')
                  .fontSize(12)
                  .fontColor('#E65100')
                  .fontWeight(FontWeight.Medium)
                ForEach(this.weatherAdvice.unsuitable, (item: string) => {
                  Text(item)
                    .fontSize(11)
                    .fontColor('#E65100')
                    .padding({ left: 6, right: 6, top: 2, bottom: 2 })
                    .backgroundColor('#FFF3E0')
                    .borderRadius(4)
                })
              }
              .width('100%')
            }
          }
          .width('100%')
          .padding(10)
          .backgroundColor(this.weatherAdvice.unsuitable.length > 0 ? '#FFF8E1' : '#E8F5E9')
          .borderRadius(8)
        }
      } else if (!this.weatherLoading) {
        Text('天气信息加载失败')
          .fontSize(14)
          .fontColor($r('app.color.text_secondary'))
      }
    }
    .width('100%')
    .padding(16)
    .backgroundColor($r('app.color.card_background'))
    .borderRadius(12)
  }

  /**
   * 构建操作类型选择器
   */
  @Builder
  buildOperationType() {
    Column({ space: 12 }) {
      Text('操作类型 *')
        .fontSize(16)
        .fontWeight(FontWeight.Bold)
        .fontColor($r('app.color.text_primary'))
        .width('100%')

      Grid() {
        ForEach(this.operationTypes, (item: OperationType) => {
          GridItem() {
            Stack({ alignContent: Alignment.TopEnd }) {
              Column({ space: 6 }) {
                Text(item.icon)
                  .fontSize(28)
                Text(item.type)
                  .fontSize(13)
                  .fontColor(this.selectedType === item.type ?
                    Color.White : $r('app.color.text_primary'))
              }
              .width('100%')
              .height(80)
              .justifyContent(FlexAlign.Center)
              .backgroundColor(this.selectedType === item.type ?
                $r('app.color.primary_professional') :
                (this.isUnsuitableOperation(item.type) ? '#FFF8E1' : $r('app.color.background')))
              .borderRadius(12)
              .borderWidth(2)
              .borderColor(this.selectedType === item.type ?
                $r('app.color.primary_professional') :
                (this.isUnsuitableOperation(item.type) ? '#FFB74D' : $r('app.color.border')))
              .onClick(() => {
                // 如果选择了不宜操作,给出提示
                if (this.isUnsuitableOperation(item.type)) {
                  promptAction.showDialog({
                    title: '天气提醒',
                    message: `当前天气(${this.currentWeather})不太适宜进行「${item.type}」操作。\n\n${this.weatherAdvice.tips}\n\n是否仍要选择此操作?`,
                    buttons: [
                      { text: '取消', color: '#666666' },
                      { text: '仍然选择', color: '#E65100' }
                    ]
                  }).then((result) => {
                    if (result.index === 1) {
                      this.selectedType = item.type;
                      if (item.template && !this.details) {
                        this.details = item.template;
                      }
                    }
                  });
                } else {
                  this.selectedType = item.type;
                  if (item.template && !this.details) {
                    this.details = item.template;
                  }
                }
              })

              // 不宜操作标识
              if (this.isUnsuitableOperation(item.type) && this.selectedType !== item.type) {
                Text('⚠️')
                  .fontSize(14)
                  .margin({ top: 4, right: 4 })
              }
            }
          }
        })
      }
      .columnsTemplate('1fr 1fr 1fr')
      .rowsGap(12)
      .columnsGap(12)
      .width('100%')
    }
    .width('100%')
    .padding(16)
    .backgroundColor($r('app.color.card_background'))
    .borderRadius(12)
  }

  /**
   * 构建基本信息表单
   */
  @Builder
  buildBasicInfo() {
    Column({ space: 12 }) {
      Text('记录信息')
        .fontSize(16)
        .fontWeight(FontWeight.Bold)
        .fontColor($r('app.color.text_primary'))
        .width('100%')

      Column({ space: 16 }) {
        // 操作日期
        Column({ space: 8 }) {
          Text('操作日期')
            .fontSize(14)
            .fontColor($r('app.color.text_secondary'))
            .width('100%')
          Text(this.formatDate(this.selectedDate))
            .fontSize(15)
            .fontColor($r('app.color.text_primary'))
            .width('100%')
            .padding(12)
            .backgroundColor($r('app.color.background'))
            .borderRadius(8)
        }
        .width('100%')

        // 操作人
        Column({ space: 8 }) {
          Text('操作人')
            .fontSize(14)
            .fontColor($r('app.color.text_secondary'))
            .width('100%')
          TextInput({ placeholder: '请输入操作人姓名', text: this.operator })
            .fontSize(15)
            .onChange((value: string) => {
              this.operator = value;
            })
        }
        .width('100%')

        // 详细说明
        Column({ space: 8 }) {
          Text('详细说明')
            .fontSize(14)
            .fontColor($r('app.color.text_secondary'))
            .width('100%')
          TextArea({ placeholder: '记录操作的详细情况...', text: this.details })
            .fontSize(15)
            .height(100)
            .onChange((value: string) => {
              this.details = value;
            })
        }
        .width('100%')

        // 成本
        Column({ space: 8 }) {
          Text('成本(元)')
            .fontSize(14)
            .fontColor($r('app.color.text_secondary'))
            .width('100%')
          TextInput({ placeholder: '例如: 150' })
            .fontSize(15)
            .type(InputType.Number)
            .onChange((value: string) => {
              this.cost = value;
            })
        }
        .width('100%')
      }
      .width('100%')
      .padding(16)
      .backgroundColor($r('app.color.background'))
      .borderRadius(8)
    }
    .width('100%')
    .padding(16)
    .backgroundColor($r('app.color.card_background'))
    .borderRadius(12)
  }

  /**
   * 构建图片选择区域
   */
  @Builder
  buildImages() {
    Column({ space: 12 }) {
      Text('现场照片')
        .fontSize(16)
        .fontWeight(FontWeight.Bold)
        .fontColor($r('app.color.text_primary'))
        .width('100%')

      if (this.selectedImages.length > 0) {
        // 已选择图片的展示
        Column({ space: 8 }) {
          Grid() {
            ForEach(this.selectedImages, (img: ImageInfo, index: number) => {
              GridItem() {
                Stack({ alignContent: Alignment.TopEnd }) {
                  Image('file://' + img.uri)
                    .width('100%')
                    .height(100)
                    .objectFit(ImageFit.Cover)
                    .borderRadius(8)

                  // 删除按钮
                  Column() {
                    Text('×')
                      .fontSize(20)
                      .fontColor(Color.White)
                      .fontWeight(FontWeight.Bold)
                  }
                  .width(24)
                  .height(24)
                  .justifyContent(FlexAlign.Center)
                  .backgroundColor('#EF5350')
                  .borderRadius(12)
                  .margin({ top: 4, right: 4 })
                  .shadow({ radius: 2, color: '#00000040', offsetX: 0, offsetY: 1 })
                  .onClick(() => {
                    this.selectedImages.splice(index, 1);
                  })
                }
              }
            })
          }
          .columnsTemplate('1fr 1fr 1fr')
          .rowsGap(8)
          .columnsGap(8)
          .width('100%')

          Button('+ 添加更多照片')
            .width('100%')
            .height(40)
            .fontSize(14)
            .backgroundColor($r('app.color.background'))
            .fontColor($r('app.color.primary_professional'))
            .borderRadius(8)
            .onClick(() => {
              this.showImagePickerMenu();
            })
        }
        .width('100%')
        .padding(16)
        .backgroundColor($r('app.color.background'))
        .borderRadius(8)
      } else {
        // 未选择图片的空状态
        Column({ space: 12 }) {
          Text('📷')
            .fontSize(48)
          Text('添加现场照片')
            .fontSize(14)
            .fontColor($r('app.color.text_secondary'))
          Button('选择照片')
            .fontSize(14)
            .height(40)
            .width(120)
            .backgroundColor($r('app.color.primary_professional'))
            .fontColor(Color.White)
            .borderRadius(20)
            .onClick(() => {
              this.showImagePickerMenu();
            })
        }
        .width('100%')
        .height(180)
        .justifyContent(FlexAlign.Center)
        .backgroundColor($r('app.color.background'))
        .borderRadius(8)
        .borderWidth(1)
        .borderColor($r('app.color.border'))
        .borderStyle(BorderStyle.Dashed)
        .padding(16)
      }
    }
    .width('100%')
    .padding(16)
    .backgroundColor($r('app.color.card_background'))
    .borderRadius(12)
  }

  /**
   * 构建底部按钮栏
   */
  @Builder
  buildFooter() {
    Row({ space: 12 }) {
      Button('取消')
        .width('30%')
        .height(48)
        .fontSize(16)
        .backgroundColor($r('app.color.background'))
        .fontColor($r('app.color.text_primary'))
        .borderRadius(24)
        .onClick(() => {
          router.back();
        })

      Button(this.isSaving ? '保存中...' : '保存')
        .layoutWeight(1)
        .height(48)
        .fontSize(16)
        .fontWeight(FontWeight.Medium)
        .backgroundColor(this.canSave() ?
          $r('app.color.primary_professional') : $r('app.color.border'))
        .fontColor(Color.White)
        .borderRadius(24)
        .enabled(this.canSave() && !this.isSaving)
        .onClick(async () => {
          await this.saveOperation();
        })
    }
    .width('100%')
    .padding({ left: 16, right: 16, top: 12, bottom: 12 })
    .backgroundColor($r('app.color.card_background'))
    .shadow({ radius: 8, color: $r('app.color.shadow_light'), offsetY: -2 })
  }

  /**
   * 显示图片选择菜单
   */
  private showImagePickerMenu(): void {
    promptAction.showActionMenu({
      title: '选择图片来源',
      buttons: [
        { text: '拍照', color: '#000000' },
        { text: '从相册选择', color: '#000000' }
      ]
    }).then(async (result) => {
      if (result.index === 0) {
        await this.takePicture();
      } else if (result.index === 1) {
        await this.pickFromGallery();
      }
    }).catch((error: Error) => {
      console.error('Action menu error:', error);
    });
  }

  /**
   * 拍照
   */
  private async takePicture(): Promise<void> {
    try {
      const result: ImagePickResult = await this.imageService.takePicture();
      if (result.success && result.imageInfo) {
        this.selectedImages.push(result.imageInfo);
        promptAction.showToast({ message: '照片已添加', duration: 2000 });
      } else if (result.error) {
        promptAction.showToast({ message: result.error, duration: 2000 });
      }
    } catch (error) {
      console.error('Failed to take picture:', error);
      promptAction.showToast({ message: '拍照失败', duration: 2000 });
    }
  }

  /**
   * 从相册选择
   */
  private async pickFromGallery(): Promise<void> {
    try {
      const result: ImagePickResult = await this.imageService.pickFromGallery();
      if (result.success && result.imageInfo) {
        this.selectedImages.push(result.imageInfo);
        promptAction.showToast({ message: '图片已添加', duration: 2000 });
      } else if (result.error) {
        promptAction.showToast({ message: result.error, duration: 2000 });
      }
    } catch (error) {
      console.error('Failed to pick from gallery:', error);
      promptAction.showToast({ message: '选择图片失败', duration: 2000 });
    }
  }

  /**
   * 表单验证
   */
  private canSave(): boolean {
    return this.selectedType.trim().length > 0 &&
      this.operator.trim().length > 0;
  }

  /**
   * 保存农事记录
   */
  private async saveOperation(): Promise<void> {
    if (!this.canSave()) {
      return;
    }

    this.isSaving = true;

    try {
      const operation: FarmOperation = {
        id: this.fieldService.generateOperationId(),
        fieldId: this.fieldId,
        operationType: this.selectedType,
        date: this.selectedDate,
        operator: this.operator.trim(),
        details: this.details.trim(),
        cost: this.cost.trim() ? parseFloat(this.cost) : undefined,
        images: this.selectedImages,
        weather: this.currentWeather,
        createdAt: Date.now()
      };

      const success = await this.fieldService.addFarmOperation(this.fieldId, operation);

      if (success) {
        promptAction.showToast({ message: '农事记录添加成功', duration: 2000 });
        router.back();
      } else {
        throw Error('保存失败');
      }
    } catch (error) {
      console.error('Failed to save operation:', error);
      promptAction.showToast({ message: '保存失败,请重试', duration: 2000 });
    } finally {
      this.isSaving = false;
    }
  }

  /**
   * 格式化日期时间
   */
  private formatDate(timestamp: number): string {
    const date = new Date(timestamp);
    const year = date.getFullYear();
    const month = (date.getMonth() + 1).toString().padStart(2, '0');
    const day = date.getDate().toString().padStart(2, '0');
    const hours = date.getHours().toString().padStart(2, '0');
    const minutes = date.getMinutes().toString().padStart(2, '0');
    return `${year}-${month}-${day} ${hours}:${minutes}`;
  }
}

页面功能说明

功能 实现方式
天气集成 使用高德天气API获取实时天气
智能建议 根据天气条件自动生成农事操作建议
不宜操作警告 选择不宜操作时弹出确认对话框
操作类型选择 九宫格布局,带图标和模板
图片管理 支持拍照和相册选择,可删除
表单验证 验证必填字段后才能保存

五、创建编辑农事记录页面

编辑页面允许用户查看和修改已有的农事记录。

5.1 创建 EditFarmOperationPage.ets

文件位置entry/src/main/ets/pages/Management/EditFarmOperationPage.ets

操作步骤

  1. entry/src/main/ets/pages/Management/ 目录下创建新文件
  2. 命名为 EditFarmOperationPage.ets
  3. 输入以下代码
/**
 * 编辑农事记录页面
 * 允许查看和修改已有的农事操作记录
 */

import { router } from '@kit.ArkUI';
import { promptAction } from '@kit.ArkUI';
import { FarmOperation, FieldInfo } from '../../models/ProfessionalAgricultureModels';
import { ImageInfo } from '../../models/CommonModels';
import { FieldService } from '../../services/FieldService';
import { ImageService, ImagePickResult } from '../../services/ImageService';
import { common } from '@kit.AbilityKit';

@Entry
@ComponentV2
struct EditFarmOperationPage {
  // 状态变量
  @Local operation: FarmOperation | null = null;   // 农事记录
  @Local field: FieldInfo | null = null;           // 地块信息
  @Local isLoading: boolean = true;                 // 加载状态
  @Local isSaving: boolean = false;                 // 保存状态
  @Local isDeleting: boolean = false;               // 删除状态

  // 表单数据
  @Local operationType: string = '';
  @Local operationDate: number = Date.now();
  @Local operator: string = '';
  @Local details: string = '';
  @Local cost: string = '';
  @Local notes: string = '';
  @Local selectedImages: ImageInfo[] = [];

  // 服务实例
  private fieldService = FieldService.getInstance();
  private imageService = ImageService.getInstance();
  private fieldId: string = '';
  private operationId: string = '';

  /**
   * 页面初始化
   */
  async aboutToAppear(): Promise<void> {
    const params = router.getParams() as Record<string, Object>;
    if (params) {
      this.fieldId = params['fieldId'] as string;
      this.operationId = params['operationId'] as string;
    }

    // 初始化ImageService
    const context = getContext(this) as common.UIAbilityContext;
    this.imageService.initialize(context);

    await this.loadData();
  }

  /**
   * 加载数据
   */
  async loadData(): Promise<void> {
    try {
      // 加载地块信息
      this.field = await this.fieldService.getFieldById(this.fieldId);

      // 加载农事记录
      const operations = await this.fieldService.getFieldOperations(this.fieldId);
      this.operation = operations.find(op => op.id === this.operationId) || null;

      if (this.operation) {
        // 填充表单数据
        this.operationType = this.operation.operationType;
        this.operationDate = this.operation.date;
        this.operator = this.operation.operator;
        this.details = this.operation.details || '';
        this.cost = this.operation.cost?.toString() || '';
        this.notes = this.operation.notes || '';
        this.selectedImages = this.operation.images || [];
      }

      this.isLoading = false;
    } catch (error) {
      console.error('[EditFarmOperationPage] Failed to load data:', error);
      promptAction.showToast({ message: '加载失败', duration: 2000 });
      router.back();
    }
  }

  build() {
    Column() {
      this.buildHeader()

      if (this.isLoading) {
        this.buildLoading()
      } else if (!this.operation) {
        this.buildNotFound()
      } else {
        Scroll() {
          Column({ space: 16 }) {
            this.buildOperationInfo()
            this.buildEditForm()
            this.buildImages()
          }
          .padding({ left: 16, right: 16, top: 16, bottom: 80 })
        }
        .layoutWeight(1)

        this.buildFooter()
      }
    }
    .width('100%')
    .height('100%')
    .backgroundColor($r('app.color.background'))
  }

  /**
   * 构建顶部导航栏
   */
  @Builder
  buildHeader() {
    Row() {
      Button('< 返回')
        .backgroundColor(Color.Transparent)
        .fontColor($r('app.color.text_primary'))
        .onClick(() => {
          router.back();
        })

      Text('农事记录详情')
        .fontSize(18)
        .fontWeight(FontWeight.Bold)
        .layoutWeight(1)
        .textAlign(TextAlign.Center)

      Text('     ')
        .width(60)
    }
    .width('100%')
    .height(56)
    .padding({ left: 16, right: 16 })
    .backgroundColor($r('app.color.card_background'))
    .shadow({ radius: 4, color: $r('app.color.shadow_light'), offsetY: 2 })
  }

  /**
   * 构建加载状态
   */
  @Builder
  buildLoading() {
    Column() {
      LoadingProgress()
        .width(48)
        .height(48)
        .color($r('app.color.primary_professional'))
      Text('加载中...')
        .fontSize(14)
        .fontColor($r('app.color.text_secondary'))
        .margin({ top: 16 })
    }
    .width('100%')
    .layoutWeight(1)
    .justifyContent(FlexAlign.Center)
  }

  /**
   * 构建未找到状态
   */
  @Builder
  buildNotFound() {
    Column({ space: 16 }) {
      Text('📋')
        .fontSize(48)
      Text('记录不存在')
        .fontSize(16)
        .fontColor($r('app.color.text_primary'))
      Button('返回')
        .onClick(() => router.back())
    }
    .width('100%')
    .layoutWeight(1)
    .justifyContent(FlexAlign.Center)
  }

  /**
   * 构建操作信息展示
   */
  @Builder
  buildOperationInfo() {
    Column({ space: 12 }) {
      Row() {
        Text(this.getOperationIcon(this.operationType))
          .fontSize(32)
        Column({ space: 4 }) {
          Text(this.operationType)
            .fontSize(20)
            .fontWeight(FontWeight.Bold)
            .fontColor($r('app.color.text_primary'))
          if (this.field) {
            Text(this.field.name)
              .fontSize(14)
              .fontColor($r('app.color.text_secondary'))
          }
        }
        .alignItems(HorizontalAlign.Start)
        .margin({ left: 12 })
        .layoutWeight(1)
      }
      .width('100%')

      Divider()

      Row({ space: 24 }) {
        Column({ space: 4 }) {
          Text('操作人')
            .fontSize(12)
            .fontColor($r('app.color.text_secondary'))
          Text(this.operator)
            .fontSize(14)
            .fontColor($r('app.color.text_primary'))
        }
        .alignItems(HorizontalAlign.Start)

        Column({ space: 4 }) {
          Text('日期')
            .fontSize(12)
            .fontColor($r('app.color.text_secondary'))
          Text(this.formatDate(this.operationDate))
            .fontSize(14)
            .fontColor($r('app.color.text_primary'))
        }
        .alignItems(HorizontalAlign.Start)

        if (this.operation?.weather) {
          Column({ space: 4 }) {
            Text('天气')
              .fontSize(12)
              .fontColor($r('app.color.text_secondary'))
            Text(this.operation.weather)
              .fontSize(14)
              .fontColor($r('app.color.text_primary'))
          }
          .alignItems(HorizontalAlign.Start)
        }
      }
      .width('100%')
    }
    .width('100%')
    .padding(16)
    .backgroundColor($r('app.color.card_background'))
    .borderRadius(12)
  }

  /**
   * 构建编辑表单
   */
  @Builder
  buildEditForm() {
    Column({ space: 12 }) {
      Text('编辑信息')
        .fontSize(16)
        .fontWeight(FontWeight.Bold)
        .fontColor($r('app.color.text_primary'))
        .width('100%')

      Column({ space: 16 }) {
        // 详细说明
        Column({ space: 8 }) {
          Text('详细说明')
            .fontSize(14)
            .fontColor($r('app.color.text_secondary'))
            .width('100%')
          TextArea({ placeholder: '记录操作的详细情况...', text: this.details })
            .fontSize(15)
            .height(100)
            .onChange((value: string) => {
              this.details = value;
            })
        }
        .width('100%')

        // 成本
        Column({ space: 8 }) {
          Text('成本(元)')
            .fontSize(14)
            .fontColor($r('app.color.text_secondary'))
            .width('100%')
          TextInput({ placeholder: '例如: 150', text: this.cost })
            .fontSize(15)
            .type(InputType.Number)
            .onChange((value: string) => {
              this.cost = value;
            })
        }
        .width('100%')

        // 备注
        Column({ space: 8 }) {
          Text('备注')
            .fontSize(14)
            .fontColor($r('app.color.text_secondary'))
            .width('100%')
          TextArea({ placeholder: '添加备注信息...', text: this.notes })
            .fontSize(15)
            .height(80)
            .onChange((value: string) => {
              this.notes = value;
            })
        }
        .width('100%')
      }
      .width('100%')
      .padding(16)
      .backgroundColor($r('app.color.background'))
      .borderRadius(8)
    }
    .width('100%')
    .padding(16)
    .backgroundColor($r('app.color.card_background'))
    .borderRadius(12)
  }

  /**
   * 构建图片展示
   */
  @Builder
  buildImages() {
    if (this.selectedImages.length === 0) {
      return;
    }

    Column({ space: 12 }) {
      Text('现场照片')
        .fontSize(16)
        .fontWeight(FontWeight.Bold)
        .fontColor($r('app.color.text_primary'))
        .width('100%')

      Grid() {
        ForEach(this.selectedImages, (img: ImageInfo, index: number) => {
          GridItem() {
            Stack({ alignContent: Alignment.TopEnd }) {
              Image('file://' + img.uri)
                .width('100%')
                .height(120)
                .objectFit(ImageFit.Cover)
                .borderRadius(8)
                .onClick(() => {
                  // 点击图片可以预览(此处简化)
                })
            }
          }
        })
      }
      .columnsTemplate('1fr 1fr 1fr')
      .rowsGap(8)
      .columnsGap(8)
      .width('100%')
    }
    .width('100%')
    .padding(16)
    .backgroundColor($r('app.color.card_background'))
    .borderRadius(12)
  }

  /**
   * 构建底部按钮栏
   */
  @Builder
  buildFooter() {
    Column({ space: 12 }) {
      // 保存按钮
      Button(this.isSaving ? '保存中...' : '保存修改')
        .width('100%')
        .height(48)
        .fontSize(16)
        .fontWeight(FontWeight.Medium)
        .backgroundColor($r('app.color.primary_professional'))
        .fontColor(Color.White)
        .borderRadius(24)
        .enabled(!this.isSaving)
        .onClick(async () => {
          await this.saveChanges();
        })

      // 删除按钮
      Button(this.isDeleting ? '删除中...' : '删除记录')
        .width('100%')
        .height(44)
        .fontSize(14)
        .backgroundColor(Color.Transparent)
        .fontColor('#EF5350')
        .borderRadius(22)
        .borderWidth(1)
        .borderColor('#EF5350')
        .enabled(!this.isDeleting)
        .onClick(() => {
          this.confirmDelete();
        })
    }
    .width('100%')
    .padding({ left: 16, right: 16, top: 12, bottom: 12 })
    .backgroundColor($r('app.color.card_background'))
    .shadow({ radius: 8, color: $r('app.color.shadow_light'), offsetY: -2 })
  }

  /**
   * 保存修改
   */
  private async saveChanges(): Promise<void> {
    if (!this.operation || !this.field) {
      return;
    }

    this.isSaving = true;

    try {
      // 更新操作记录
      this.operation.details = this.details.trim();
      this.operation.cost = this.cost.trim() ? parseFloat(this.cost) : undefined;
      this.operation.notes = this.notes.trim();

      // 保存到服务
      const success = await this.fieldService.updateField(this.field);

      if (success) {
        promptAction.showToast({ message: '保存成功', duration: 2000 });
        router.back();
      } else {
        throw Error('保存失败');
      }
    } catch (error) {
      console.error('Failed to save changes:', error);
      promptAction.showToast({ message: '保存失败,请重试', duration: 2000 });
    } finally {
      this.isSaving = false;
    }
  }

  /**
   * 确认删除
   */
  private confirmDelete(): void {
    promptAction.showDialog({
      title: '确认删除',
      message: '删除后无法恢复,确定要删除这条农事记录吗?',
      buttons: [
        { text: '取消', color: '#666666' },
        { text: '删除', color: '#EF5350' }
      ]
    }).then(async (result) => {
      if (result.index === 1) {
        await this.deleteOperation();
      }
    });
  }

  /**
   * 删除操作记录
   */
  private async deleteOperation(): Promise<void> {
    if (!this.operation || !this.field) {
      return;
    }

    this.isDeleting = true;

    try {
      // 从数组中移除
      if (this.field.operations) {
        this.field.operations = this.field.operations.filter(op => op.id !== this.operationId);
      }

      // 保存到服务
      const success = await this.fieldService.updateField(this.field);

      if (success) {
        promptAction.showToast({ message: '删除成功', duration: 2000 });
        router.back();
      } else {
        throw Error('删除失败');
      }
    } catch (error) {
      console.error('Failed to delete operation:', error);
      promptAction.showToast({ message: '删除失败,请重试', duration: 2000 });
    } finally {
      this.isDeleting = false;
    }
  }

  /**
   * 获取操作类型图标
   */
  private getOperationIcon(type: string): string {
    const iconMap: Record<string, string> = {
      '播种': '🌱',
      '施肥': '🌿',
      '浇水': '💧',
      '除草': '🌾',
      '打药': '🛡️',
      '收获': '🌻',
      '其他': '📋'
    };
    return iconMap[type] || '📝';
  }

  /**
   * 格式化日期
   */
  private formatDate(timestamp: number): string {
    const date = new Date(timestamp);
    const year = date.getFullYear();
    const month = (date.getMonth() + 1).toString().padStart(2, '0');
    const day = date.getDate().toString().padStart(2, '0');
    return `${year}-${month}-${day}`;
  }
}

六、配置页面路由

main_pages.json 中添加农事记录相关页面的路由。

文件位置entry/src/main/resources/base/profile/main_pages.json

{
  "src": [
    "pages/WelcomePage",
    "pages/Index",
    "pages/Map/FieldMapPage",
    "pages/Management/FieldManagementPage",
    "pages/Management/AddFieldPage",
    "pages/Management/EditFieldPage",
    "pages/Management/FieldDetailPage",
    "pages/Management/FarmOperationPage",
    "pages/Management/AddFarmOperationPage",
    "pages/Management/EditFarmOperationPage",
    "pages/Management/CropManagementPage",
    "pages/Management/AddCropPage",
    "pages/Management/CropDetailPage",
    "pages/OnboardingFlow/ModeSelectionPage",
    "pages/OnboardingFlow/LocationPage",
    "pages/OnboardingFlow/GoalsPage"
  ]
}

七、运行与测试

7.1 测试步骤

  1. 启动应用

    • 点击运行按钮或按 Shift + F10
    • 等待应用编译并安装到模拟器
  2. 进入农事记录页面

    • 进入地图首页
    • 点击"地块管理"
    • 选择一个地块
    • 点击"农事记录"
  3. 添加农事记录

    • 点击右上角"+ 添加"按钮
    • 查看天气建议
    • 选择操作类型(尝试选择不宜操作,查看警告提示)
    • 填写操作人、详细说明、成本
    • 添加照片(可选)
    • 点击"保存"
  4. 查看记录列表

    • 返回记录列表
    • 查看新添加的记录
    • 使用类型筛选器筛选
    • 使用搜索框搜索
  5. 编辑农事记录

    • 点击记录卡片进入详情页
    • 修改详细说明或备注
    • 点击"保存修改"
    • 尝试删除记录

7.2 预期效果

功能 预期效果
记录列表 显示所有农事记录,最新的在前
类型筛选 点击不同类型按钮筛选记录
搜索功能 输入关键词模糊搜索
天气建议 显示当前天气和农事操作建议
不宜操作警告 选择不宜操作时弹出确认对话框
照片管理 支持拍照和相册选择
保存功能 保存成功后返回列表并刷新
删除功能 删除确认对话框,删除成功后返回

八、常见问题与解决方案

8.1 农事记录保存失败

问题:点击保存后提示"保存失败"

解决方案

  1. 检查是否已创建地块
  2. 确认必填字段(操作类型、操作人)已填写
  3. 查看控制台错误日志

8.2 天气信息不显示

问题:天气提示卡片显示"天气信息加载失败"

解决方案

  1. 检查网络连接
  2. 确认高德API Key配置正确
  3. 检查位置权限是否已授予

8.3 图片无法上传

问题:点击拍照或相册选择后没有反应

解决方案

  1. 检查相机和存储权限
  2. 确认ImageService已正确初始化
  3. 查看控制台错误日志

8.4 记录列表为空

问题:添加记录后列表仍显示为空

解决方案

  1. 确认保存成功(查看Toast提示)
  2. 下拉刷新或重新进入页面
  3. 检查数据是否正确保存到存储

九、总结

本篇教程完成了:

  • ✅ 农事操作数据模型理解
  • ✅ FieldService中的农事操作方法
  • ✅ 农事记录列表页面(支持筛选和搜索)
  • ✅ 添加农事记录页面(集成天气服务)
  • ✅ 编辑农事记录页面(支持修改和删除)
  • ✅ 天气智能建议功能
  • ✅ 图片管理功能
  • ✅ 页面路由配置

关键技术点

技术点 说明
数据关联 农事记录作为地块的子数据存储
天气集成 使用高德天气API获取实时天气
智能建议 根据天气条件生成农事操作建议
图片管理 使用ImageService处理拍照和相册选择
列表筛选 支持按类型筛选和关键词搜索
卡片设计 使用Stack组件实现不宜操作标识

十、下一步

在下一篇教程中,我们将学习:

  • 任务数据模型设计
  • 任务管理服务实现
  • 任务列表与筛选
  • 任务提醒通知
  • 自动任务生成逻辑

教程版本:v1.0
更新日期:2026-01
适用版本:DevEco Studio 5.0+, HarmonyOS API 17+

Logo

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

更多推荐