第12篇:成本核算系统
教程目标
通过本篇教程,你将学会:
- 理解成本核算数据模型设计
- 实现成本记录的增删改查
- 创建成本核算列表页面
- 创建添加成本记录页面
- 实现成本统计与分析功能
- 管理成本分类与筛选
完成本教程后,你将拥有完整的农业成本核算管理功能。
一、成本核算数据模型
在实现成本核算功能之前,我们需要了解成本数据模型的结构。成本核算服务已在 CostAccountingService.ets 中实现。
1.1 查看成本数据模型
文件位置 :entry/src/main/ets/services/CostAccountingService.ets(第8-52行)
操作说明:
- 打开
entry/src/main/ets/services/CostAccountingService.ets - 查看成本相关的接口定义
typescript
/**
* 成本记录接口
*/
export interface CostAccountingRecord {
id: string; // 记录唯一标识
fieldId: string; // 关联地块ID
fieldName: string; // 地块名称
category: string; // 成本分类(种子/肥料/农药/人工)
item: string; // 具体项目名称
quantity: number; // 数量
unit: string; // 单位
unitPrice: number; // 单价
totalCost: number; // 总成本
date: number; // 发生日期
supplier?: string; // 供应商(可选)
notes?: string; // 备注(可选)
createdAt: number; // 创建时间
}
/**
* 成本统计信息接口
*/
export interface CostStatistics {
totalCost: number; // 总成本
recordCount: number; // 记录数量
categoryBreakdown: CategoryCost[]; // 分类成本明细
monthlyTrend: MonthlyCost[]; // 月度成本趋势
}
/**
* 分类成本接口
*/
export interface CategoryCost {
category: string; // 成本类别
cost: number; // 类别总成本
percentage: number; // 占比百分比
}
/**
* 月度成本接口
*/
export interface MonthlyCost {
month: string; // 月份(格式:YYYY-MM)
cost: number; // 该月总成本
}
模型设计要点:
| 设计要点 | 说明 |
|---|---|
| 地块关联 | 通过fieldId和fieldName关联地块,便于按地块统计成本 |
| 自动计算 | totalCost = quantity × unitPrice,提高录入效率 |
| 分类管理 | 支持种子、肥料、农药、人工、机械、水电等多种成本类型 |
| 统计分析 | 提供分类占比、月度趋势等多维度统计数据 |
| 可选字段 | 使用?标记非必填字段(如supplier、notes) |
1.2 成本分类说明
本系统支持以下成本分类:
成本分类体系:
├── 种子: 🌱 (玉米种子、小麦种子、水稻种子等)
├── 肥料: 🌿 (复合肥、尿素、磷肥、钾肥等)
├── 农药: 🛡️ (杀虫剂、杀菌剂、除草剂等)
├── 人工: 👨🌾 (播种用工、施肥用工、收获用工等)
├── 机械: 🚜 (耕地费、播种费、收割费等)
├── 水电: 💧 (灌溉用水、生产用电等)
└── 其他: 📋 (农膜、大棚材料、工具设备等)
每个分类都有对应的项目选项和单位选项,方便快速录入。
二、成本核算服务
CostAccountingService提供了完整的成本管理功能,包括基础CRUD、筛选查询和统计分析。
2.1 CostAccountingService核心方法
文件位置 :entry/src/main/ets/services/CostAccountingService.ets
基础CRUD方法:
typescript
/**
* 获取所有成本记录
*/
async getAllRecords(): Promise<CostAccountingRecord[]>
/**
* 添加成本记录
*/
async addRecord(record: CostAccountingRecord): Promise<boolean>
/**
* 更新成本记录
*/
async updateRecord(record: CostAccountingRecord): Promise<boolean>
/**
* 删除成本记录
*/
async deleteRecord(id: string): Promise<boolean>
筛选查询方法:
typescript
/**
* 根据分类获取记录
* @param category 成本分类('全部'返回所有记录)
*/
async getRecordsByCategory(category: string): Promise<CostAccountingRecord[]>
/**
* 根据地块获取记录
* @param fieldId 地块ID
*/
async getRecordsByField(fieldId: string): Promise<CostAccountingRecord[]>
统计分析方法:
typescript
/**
* 获取统计信息
* 包含总成本、记录数、分类占比、月度趋势
*/
async getStatistics(): Promise<CostStatistics>
/**
* 获取本月成本
*/
async getMonthlyTotalCost(): Promise<number>
/**
* 获取地块成本统计
* @param fieldId 地块ID
*/
async getFieldCostStatistics(fieldId: string): Promise<number>
2.2 统计功能实现原理
分类成本统计:
typescript
// 使用Map结构累加各分类的成本
const categoryMap = new Map<string, number>();
for (const record of records) {
const current = categoryMap.get(record.category) || 0;
categoryMap.set(record.category, current + record.totalCost);
}
// 计算百分比并排序
categoryBreakdown.sort((a, b) => b.cost - a.cost); // 按成本降序
月度趋势统计:
typescript
// 初始化最近6个月的数据结构
const monthlyMap = new Map<string, number>();
for (let i = 5; i >= 0; i--) {
const date = new Date(now.getFullYear(), now.getMonth() - i, 1);
const monthKey = `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}`;
monthlyMap.set(monthKey, 0);
}
// 累加各月的成本
for (const record of records) {
const date = new Date(record.date);
const monthKey = `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}`;
if (monthlyMap.has(monthKey)) {
const current = monthlyMap.get(monthKey) || 0;
monthlyMap.set(monthKey, current + record.totalCost);
}
}
三、创建成本核算列表页面
现在开始实现成本核算的列表页面,展示成本记录和统计数据。
3.1 创建页面文件
文件路径 :entry/src/main/ets/pages/Services/CostAccountingPage.ets
页面功能:
- 展示总成本统计卡片
- 支持按分类筛选记录
- 显示成本分类占比分析
- 长按删除成本记录
- 点击进入编辑页面
3.2 完整页面代码
typescript
/**
* 成本核算页面
* 展示和管理农业生产成本
*/
import { router } from '@kit.ArkUI';
import { promptAction } from '@kit.ArkUI';
import { CostAccountingService, CostAccountingRecord, CostStatistics, CategoryCost } from '../../services/CostAccountingService';
@Entry
@ComponentV2
export struct CostAccountingPage {
// ===== 状态变量 =====
@Local costRecords: CostAccountingRecord[] = []; // 成本记录列表
@Local statistics: CostStatistics | null = null; // 统计数据
@Local selectedCategory: string = '全部'; // 选中的分类
@Local isLoading: boolean = true; // 加载状态
@Local showAnalysis: boolean = false; // 是否显示分析图表
// ===== 服务实例 =====
private costService: CostAccountingService = CostAccountingService.getInstance();
// ===== 成本分类选项 =====
private costCategories = ['全部', '种子', '肥料', '农药', '人工', '机械', '水电', '其他'];
/**
* 生命周期:页面即将出现
*/
aboutToAppear(): void {
this.loadData();
}
/**
* 生命周期:页面显示时刷新数据
*/
onPageShow(): void {
this.loadData();
}
/**
* 加载成本数据
*/
async loadData(): Promise<void> {
try {
this.costRecords = await this.costService.getAllRecords();
this.statistics = await this.costService.getStatistics();
this.isLoading = false;
} catch (error) {
console.error('Failed to load cost data:', error);
this.isLoading = false;
}
}
/**
* 页面主体结构
*/
build() {
Column() {
this.buildHeader()
if (this.isLoading) {
this.buildLoading()
} else {
Column() {
this.buildSummary()
if (this.showAnalysis && this.statistics) {
this.buildAnalysis()
}
this.buildCategoryFilter()
this.buildCostList()
}
.layoutWeight(1)
}
this.buildAddButton()
}
.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)
// 分析按钮
Button(this.showAnalysis ? '隐藏' : '分析')
.backgroundColor(Color.Transparent)
.fontColor($r('app.color.primary_professional'))
.onClick(() => {
this.showAnalysis = !this.showAnalysis;
})
}
.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
buildSummary() {
Column({ space: 12 }) {
Text('总成本')
.fontSize(14)
.fontColor($r('app.color.text_secondary'))
// 总成本金额
Text(`¥${this.statistics ? this.statistics.totalCost.toFixed(2) : '0.00'}`)
.fontSize(32)
.fontWeight(FontWeight.Bold)
.fontColor('#FF6B6B') // 红色表示支出
// 记录数量和类别数量
Row({ space: 16 }) {
Text(`${this.statistics ? this.statistics.recordCount : 0} 条记录`)
.fontSize(14)
.fontColor($r('app.color.text_secondary'))
if (this.statistics && this.statistics.categoryBreakdown.length > 0) {
Text(`${this.statistics.categoryBreakdown.length} 个类别`)
.fontSize(14)
.fontColor($r('app.color.text_secondary'))
}
}
}
.width('100%')
.padding(24)
.backgroundColor($r('app.color.card_background'))
.margin(16)
.borderRadius(12)
}
/**
* 构建成本分析图表
*/
@Builder
buildAnalysis() {
Column({ space: 16 }) {
// 标题
Text('成本分类占比')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor($r('app.color.text_primary'))
.width('100%')
// 分类成本列表
if (this.statistics && this.statistics.categoryBreakdown.length > 0) {
Column({ space: 8 }) {
ForEach(this.statistics.categoryBreakdown, (item: CategoryCost) => {
Row({ space: 12 }) {
// 分类图标
Text(this.getCategoryIcon(item.category))
.fontSize(16)
// 分类名称
Text(item.category)
.fontSize(14)
.fontColor($r('app.color.text_primary'))
.width(60)
// 进度条
Stack() {
// 背景条
Row()
.width('100%')
.height(8)
.backgroundColor('#E0E0E0')
.borderRadius(4)
// 进度条
Row()
.width(`${item.percentage}%`)
.height(8)
.backgroundColor(this.getCategoryColor(item.category))
.borderRadius(4)
}
.layoutWeight(1)
// 金额
Text(`¥${item.cost.toFixed(0)}`)
.fontSize(13)
.fontColor($r('app.color.text_secondary'))
.width(70)
.textAlign(TextAlign.End)
}
.width('100%')
})
}
} else {
Text('暂无数据')
.fontSize(14)
.fontColor($r('app.color.text_tertiary'))
}
}
.width('100%')
.padding(16)
.backgroundColor($r('app.color.card_background'))
.margin({ left: 16, right: 16, bottom: 16 })
.borderRadius(12)
}
/**
* 构建分类筛选器
*/
@Builder
buildCategoryFilter() {
Scroll() {
Row({ space: 8 }) {
ForEach(this.costCategories, (category: string) => {
Text(category)
.fontSize(14)
.fontColor(this.selectedCategory === category ?
Color.White : $r('app.color.text_primary'))
.padding({ left: 16, right: 16, top: 8, bottom: 8 })
.backgroundColor(this.selectedCategory === category ?
$r('app.color.primary_professional') : $r('app.color.card_background'))
.borderRadius(16)
.onClick(() => {
this.selectedCategory = category;
})
})
}
.padding({ left: 16, right: 16 })
}
.scrollable(ScrollDirection.Horizontal)
.scrollBar(BarState.Off)
.width('100%')
.margin({ bottom: 16 })
}
/**
* 构建成本记录列表
*/
@Builder
buildCostList() {
if (this.getFilteredCostRecords().length === 0) {
// 空状态
Column({ space: 16 }) {
Text('💰')
.fontSize(48)
Text('还没有成本记录')
.fontSize(16)
.fontColor($r('app.color.text_secondary'))
Text('点击下方按钮添加第一条记录')
.fontSize(14)
.fontColor($r('app.color.text_tertiary'))
}
.width('100%')
.layoutWeight(1)
.justifyContent(FlexAlign.Center)
} else {
// 记录列表
Scroll() {
Column({ space: 12 }) {
ForEach(this.getFilteredCostRecords(), (record: CostAccountingRecord) => {
this.buildCostCard(record)
})
}
.padding({ left: 16, right: 16, bottom: 16 })
}
.layoutWeight(1)
.scrollBar(BarState.Auto)
}
}
/**
* 构建单个成本记录卡片
*/
@Builder
buildCostCard(record: CostAccountingRecord) {
Column({ space: 12 }) {
// 第一行:分类图标、项目名称、总成本
Row() {
Text(this.getCategoryIcon(record.category))
.fontSize(20)
Column({ space: 4 }) {
Text(record.item)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor($r('app.color.text_primary'))
Text(record.category)
.fontSize(12)
.fontColor($r('app.color.text_secondary'))
}
.alignItems(HorizontalAlign.Start)
.margin({ left: 8 })
.layoutWeight(1)
Text(`¥${record.totalCost.toFixed(2)}`)
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#FF6B6B')
}
.width('100%')
// 第二行:日期、数量单价、地块
Row() {
Text(this.formatDate(record.date))
.fontSize(12)
.fontColor($r('app.color.text_tertiary'))
if (record.quantity && record.unit) {
Text(`${record.quantity} ${record.unit} × ¥${record.unitPrice}`)
.fontSize(12)
.fontColor($r('app.color.text_secondary'))
.margin({ left: 12 })
}
Blank()
Text(record.fieldName)
.fontSize(12)
.fontColor($r('app.color.primary_professional'))
}
.width('100%')
// 供应商信息(可选)
if (record.supplier) {
Row({ space: 4 }) {
Text('🏪')
.fontSize(12)
Text(record.supplier)
.fontSize(12)
.fontColor($r('app.color.text_tertiary'))
}
.width('100%')
}
// 备注信息(可选)
if (record.notes) {
Row({ space: 4 }) {
Text('📝')
.fontSize(12)
Text(record.notes)
.fontSize(12)
.fontColor($r('app.color.text_tertiary'))
.maxLines(2)
.textOverflow({ overflow: TextOverflow.Ellipsis })
}
.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/Services/EditCostRecordPage',
params: {
record: record
}
});
})
.gesture(
// 长按显示删除确认
LongPressGesture()
.onAction(() => {
this.showDeleteConfirm(record);
})
)
}
/**
* 构建添加按钮
*/
@Builder
buildAddButton() {
Button('+ 添加成本记录')
.width('100%')
.height(48)
.margin(16)
.backgroundColor($r('app.color.primary_professional'))
.fontColor(Color.White)
.borderRadius(24)
.onClick(() => {
router.pushUrl({
url: 'pages/Services/AddCostRecordPage'
});
})
}
/**
* 获取筛选后的成本记录
*/
private getFilteredCostRecords(): CostAccountingRecord[] {
if (this.selectedCategory === '全部') {
return this.costRecords;
}
return this.costRecords.filter(r => r.category === this.selectedCategory);
}
/**
* 获取分类图标
*/
private getCategoryIcon(category: string): string {
const iconMap: Record<string, string> = {
'种子': '🌱',
'肥料': '🌿',
'农药': '🛡️',
'人工': '👨🌾',
'机械': '🚜',
'水电': '💧',
'其他': '📋'
};
return iconMap[category] || '💰';
}
/**
* 获取分类颜色
*/
private getCategoryColor(category: string): string {
const colorMap: Record<string, string> = {
'种子': '#4CAF50',
'肥料': '#8BC34A',
'农药': '#FF9800',
'人工': '#2196F3',
'机械': '#9C27B0',
'水电': '#00BCD4',
'其他': '#607D8B'
};
return colorMap[category] || '#9E9E9E';
}
/**
* 格式化日期
*/
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}`;
}
/**
* 显示删除确认对话框
*/
private showDeleteConfirm(record: CostAccountingRecord): void {
promptAction.showDialog({
title: '确认删除',
message: `确定要删除成本记录 "${record.item}" 吗?`,
buttons: [
{ text: '取消', color: '#999999' },
{ text: '删除', color: '#FF6B6B' }
]
}).then(async (result) => {
if (result.index === 1) {
const success = await this.costService.deleteRecord(record.id);
if (success) {
await this.loadData();
promptAction.showToast({
message: '删除成功',
duration: 2000
});
} else {
promptAction.showToast({
message: '删除失败',
duration: 2000
});
}
}
}).catch(() => {
// 用户取消
});
}
}
3.3 页面功能说明
| 功能模块 | 实现说明 |
|---|---|
| 统计卡片 | 显示总成本、记录数、类别数,一目了然掌握成本情况 |
| 分析图表 | 点击"分析"按钮显示/隐藏,展示各类别成本占比和进度条 |
| 分类筛选 | 横向滚动的分类标签,支持全部、种子、肥料等筛选 |
| 记录卡片 | 显示项目名称、金额、日期、数量单价、地块等信息 |
| 点击编辑 | 点击卡片进入编辑页面,可修改成本记录 |
| 长按删除 | 长按卡片显示删除确认对话框,避免误删除 |
四、创建添加成本记录页面
现在实现添加成本记录的页面,包含完整的表单验证和自动计算功能。
4.1 创建页面文件
文件路径 :entry/src/main/ets/pages/Services/AddCostRecordPage.ets
页面功能:
- 选择地块
- 选择成本分类
- 选择项目名称(根据分类动态显示)
- 输入数量和单位(根据分类动态显示)
- 输入单价
- 自动计算总成本
- 表单验证
4.2 完整页面代码(分段说明)
第一部分:导入和状态定义
typescript
/**
* 添加成本记录页面
* 用于添加新的成本记录
*/
import { router } from '@kit.ArkUI';
import { promptAction } from '@kit.ArkUI';
import { FieldService } from '../../services/FieldService';
import { CostAccountingService, CostAccountingRecord } from '../../services/CostAccountingService';
import { FieldInfo, FieldType, IrrigationSystem } from '../../models/ProfessionalAgricultureModels';
@Entry
@ComponentV2
struct AddCostRecordPage {
// ===== 表单状态 =====
@Local selectedCategory: string = ''; // 选中的成本类别
@Local item: string = ''; // 项目名称
@Local quantity: string = ''; // 数量
@Local unit: string = ''; // 单位
@Local unitPrice: string = ''; // 单价
@Local supplier: string = ''; // 供应商
@Local notes: string = ''; // 备注
@Local selectedFieldId: string = ''; // 选中的地块ID
@Local selectedFieldName: string = ''; // 选中的地块名称
// ===== UI状态 =====
@Local fieldList: FieldInfo[] = []; // 地块列表
@Local showFieldSelector: boolean = false; // 是否显示地块选择器
@Local showCategorySelector: boolean = false; // 是否显示分类选择器
@Local showUnitSelector: boolean = false; // 是否显示单位选择器
@Local showItemSelector: boolean = false; // 是否显示项目选择器
// ===== 服务实例 =====
private fieldService: FieldService = FieldService.getInstance();
private costService: CostAccountingService = CostAccountingService.getInstance();
// ===== 成本分类选项 =====
private costCategories = ['种子', '肥料', '农药', '人工', '机械', '水电', '其他'];
// ===== 项目选项(根据分类动态显示) =====
private itemOptions: Record<string, string[]> = {
'种子': ['玉米种子', '小麦种子', '水稻种子', '大豆种子', '蔬菜种子', '其他种子'],
'肥料': ['复合肥', '尿素', '磷肥', '钾肥', '有机肥', '叶面肥', '其他肥料'],
'农药': ['杀虫剂', '杀菌剂', '除草剂', '植物生长调节剂', '其他农药'],
'人工': ['播种用工', '施肥用工', '除草用工', '收获用工', '日常管理', '其他用工'],
'机械': ['耕地费', '播种费', '收割费', '运输费', '灌溉费', '其他机械费'],
'水电': ['灌溉用水', '生活用水', '生产用电', '灌溉用电', '其他水电'],
'其他': ['农膜', '大棚材料', '工具设备', '包装材料', '运输费用', '其他费用']
};
// ===== 单位选项(根据分类动态显示) =====
private unitOptions: Record<string, string[]> = {
'种子': ['斤', '公斤', '袋', '包'],
'肥料': ['斤', '公斤', '袋', '吨'],
'农药': ['瓶', '袋', '升', '公斤'],
'人工': ['天', '小时', '人次'],
'机械': ['天', '小时', '亩'],
'水电': ['度', '吨', '立方米'],
'其他': ['个', '件', '次', '项']
};
/**
* 生命周期:页面即将出现
*/
aboutToAppear(): void {
this.loadFieldList();
}
/**
* 加载地块列表
*/
async loadFieldList(): Promise<void> {
try {
this.fieldList = await this.fieldService.getAllFields();
// 如果没有地块数据,添加示例数据
if (this.fieldList.length === 0) {
const demoField: FieldInfo = {
id: 'field_demo_1',
name: '东地块',
location: '村东500米',
area: 15,
fieldType: FieldType.PLAIN,
soilType: '黄土',
irrigation: IrrigationSystem.DRIP,
mechanizationLevel: '部分机械',
images: [],
createdAt: Date.now(),
updatedAt: Date.now()
};
this.fieldList = [demoField];
}
} catch (error) {
console.error('Failed to load field list:', error);
}
}
/**
* 页面主体结构
*/
build() {
Column() {
this.buildHeader()
Scroll() {
Column({ space: 16 }) {
this.buildFieldSelector()
this.buildCategorySelector()
this.buildItemInput()
this.buildQuantityInput()
this.buildPriceInput()
this.buildSupplierInput()
this.buildNotesInput()
this.buildTotalCost()
}
.padding(16)
}
.layoutWeight(1)
.scrollBar(BarState.Auto)
this.buildSubmitButton()
}
.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 })
}
第二部分:地块选择器
typescript
/**
* 构建地块选择器
*/
@Builder
buildFieldSelector() {
Column({ space: 8 }) {
Text('使用地块 *')
.fontSize(14)
.fontColor($r('app.color.text_secondary'))
.width('100%')
// 选择框
Row() {
Text(this.selectedFieldName || '请选择地块')
.fontSize(15)
.fontColor(this.selectedFieldName ? $r('app.color.text_primary') : $r('app.color.text_tertiary'))
.layoutWeight(1)
Text('▼')
.fontSize(12)
.fontColor($r('app.color.text_tertiary'))
}
.width('100%')
.height(48)
.padding({ left: 12, right: 12 })
.backgroundColor($r('app.color.card_background'))
.borderRadius(8)
.onClick(() => {
this.showFieldSelector = !this.showFieldSelector;
this.showCategorySelector = false;
})
// 下拉列表
if (this.showFieldSelector) {
Column() {
ForEach(this.fieldList, (field: FieldInfo) => {
Row() {
Column({ space: 2 }) {
Text(field.name)
.fontSize(15)
.fontColor($r('app.color.text_primary'))
Text(`${field.area}亩 · ${field.location}`)
.fontSize(12)
.fontColor($r('app.color.text_tertiary'))
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
if (this.selectedFieldId === field.id) {
Text('✓')
.fontSize(16)
.fontColor($r('app.color.primary_professional'))
}
}
.width('100%')
.padding({ left: 12, right: 12, top: 10, bottom: 10 })
.backgroundColor(this.selectedFieldId === field.id ? '#E3F2FD' : Color.Transparent)
.onClick(() => {
this.selectedFieldId = field.id;
this.selectedFieldName = field.name;
this.showFieldSelector = false;
})
})
}
.width('100%')
.backgroundColor($r('app.color.card_background'))
.borderRadius(8)
.clip(true)
}
}
.width('100%')
}
第三部分:分类选择器
typescript
/**
* 构建成本分类选择器
*/
@Builder
buildCategorySelector() {
Column({ space: 8 }) {
Text('成本类别 *')
.fontSize(14)
.fontColor($r('app.color.text_secondary'))
.width('100%')
Row() {
Text(this.selectedCategory || '请选择类别')
.fontSize(15)
.fontColor(this.selectedCategory ? $r('app.color.text_primary') : $r('app.color.text_tertiary'))
.layoutWeight(1)
Text('▼')
.fontSize(12)
.fontColor($r('app.color.text_tertiary'))
}
.width('100%')
.height(48)
.padding({ left: 12, right: 12 })
.backgroundColor($r('app.color.card_background'))
.borderRadius(8)
.onClick(() => {
this.showCategorySelector = !this.showCategorySelector;
this.showFieldSelector = false;
})
if (this.showCategorySelector) {
Column() {
ForEach(this.costCategories, (category: string) => {
Row() {
Text(this.getCategoryIcon(category))
.fontSize(16)
Text(category)
.fontSize(15)
.fontColor($r('app.color.text_primary'))
.margin({ left: 8 })
.layoutWeight(1)
if (this.selectedCategory === category) {
Text('✓')
.fontSize(16)
.fontColor($r('app.color.primary_professional'))
}
}
.width('100%')
.padding({ left: 12, right: 12, top: 10, bottom: 10 })
.backgroundColor(this.selectedCategory === category ? '#E3F2FD' : Color.Transparent)
.onClick(() => {
this.selectedCategory = category;
this.unit = ''; // 重置单位
this.item = ''; // 重置项目
this.showCategorySelector = false;
})
})
}
.width('100%')
.backgroundColor($r('app.color.card_background'))
.borderRadius(8)
.clip(true)
}
}
.width('100%')
}
第四部分:项目名称和数量输入
typescript
/**
* 构建项目名称输入框
*/
@Builder
buildItemInput() {
Column({ space: 8 }) {
Text('项目名称 *')
.fontSize(14)
.fontColor($r('app.color.text_secondary'))
.width('100%')
Row() {
Text(this.item || '请选择项目')
.fontSize(15)
.fontColor(this.item ? $r('app.color.text_primary') : $r('app.color.text_tertiary'))
.layoutWeight(1)
Text('▼')
.fontSize(12)
.fontColor($r('app.color.text_tertiary'))
}
.width('100%')
.height(48)
.padding({ left: 12, right: 12 })
.backgroundColor($r('app.color.card_background'))
.borderRadius(8)
.onClick(() => {
if (!this.selectedCategory) {
promptAction.showToast({
message: '请先选择成本类别',
duration: 2000
});
return;
}
this.showItemSelector = !this.showItemSelector;
})
// 项目选择列表
if (this.showItemSelector && this.selectedCategory) {
Column() {
ForEach(this.getItemOptions(), (itemOption: string) => {
Row() {
Text(itemOption)
.fontSize(15)
.fontColor($r('app.color.text_primary'))
.layoutWeight(1)
if (this.item === itemOption) {
Text('✓')
.fontSize(16)
.fontColor($r('app.color.primary_professional'))
}
}
.width('100%')
.padding({ left: 12, right: 12, top: 10, bottom: 10 })
.backgroundColor(this.item === itemOption ? '#E3F2FD' : Color.Transparent)
.onClick(() => {
this.item = itemOption;
this.showItemSelector = false;
})
})
}
.width('100%')
.backgroundColor($r('app.color.card_background'))
.borderRadius(8)
.clip(true)
}
}
.width('100%')
}
/**
* 构建数量与单位输入框
*/
@Builder
buildQuantityInput() {
Column({ space: 8 }) {
Text('数量与单位')
.fontSize(14)
.fontColor($r('app.color.text_secondary'))
.width('100%')
Row({ space: 12 }) {
// 数量输入框
TextInput({ placeholder: '数量', text: this.quantity })
.layoutWeight(1)
.height(48)
.backgroundColor($r('app.color.card_background'))
.borderRadius(8)
.type(InputType.NUMBER_DECIMAL)
.onChange((value: string) => {
this.quantity = value;
})
// 单位选择器
Column() {
Text(this.unit || '单位')
.fontSize(14)
.fontColor(this.unit ? $r('app.color.text_primary') : $r('app.color.text_tertiary'))
}
.width(80)
.height(48)
.backgroundColor($r('app.color.card_background'))
.borderRadius(8)
.justifyContent(FlexAlign.Center)
.onClick(() => {
if (!this.selectedCategory) {
promptAction.showToast({
message: '请先选择成本类别',
duration: 2000
});
return;
}
this.showUnitSelector = !this.showUnitSelector;
})
}
// 单位选择列表
if (this.showUnitSelector && this.selectedCategory) {
Column() {
ForEach(this.getUnitOptions(), (unitOption: string) => {
Row() {
Text(unitOption)
.fontSize(15)
.fontColor($r('app.color.text_primary'))
.layoutWeight(1)
if (this.unit === unitOption) {
Text('✓')
.fontSize(16)
.fontColor($r('app.color.primary_professional'))
}
}
.width('100%')
.padding({ left: 12, right: 12, top: 10, bottom: 10 })
.backgroundColor(this.unit === unitOption ? '#E3F2FD' : Color.Transparent)
.onClick(() => {
this.unit = unitOption;
this.showUnitSelector = false;
})
})
}
.width('100%')
.backgroundColor($r('app.color.card_background'))
.borderRadius(8)
.clip(true)
}
}
.width('100%')
}
第五部分:价格和其他信息输入
typescript
/**
* 构建单价输入框
*/
@Builder
buildPriceInput() {
Column({ space: 8 }) {
Text('单价 (元)')
.fontSize(14)
.fontColor($r('app.color.text_secondary'))
.width('100%')
TextInput({ placeholder: '请输入单价', text: this.unitPrice })
.width('100%')
.height(48)
.backgroundColor($r('app.color.card_background'))
.borderRadius(8)
.type(InputType.NUMBER_DECIMAL)
.onChange((value: string) => {
this.unitPrice = value;
})
}
.width('100%')
}
/**
* 构建供应商输入框
*/
@Builder
buildSupplierInput() {
Column({ space: 8 }) {
Text('供应商')
.fontSize(14)
.fontColor($r('app.color.text_secondary'))
.width('100%')
TextInput({ placeholder: '选填', text: this.supplier })
.width('100%')
.height(48)
.backgroundColor($r('app.color.card_background'))
.borderRadius(8)
.onChange((value: string) => {
this.supplier = value;
})
}
.width('100%')
}
/**
* 构建备注输入框
*/
@Builder
buildNotesInput() {
Column({ space: 8 }) {
Text('备注')
.fontSize(14)
.fontColor($r('app.color.text_secondary'))
.width('100%')
TextInput({ placeholder: '选填', text: this.notes })
.width('100%')
.height(48)
.backgroundColor($r('app.color.card_background'))
.borderRadius(8)
.onChange((value: string) => {
this.notes = value;
})
}
.width('100%')
}
/**
* 构建总成本显示
*/
@Builder
buildTotalCost() {
Row() {
Text('总成本')
.fontSize(16)
.fontColor($r('app.color.text_primary'))
Blank()
// 自动计算并显示总成本
Text(`¥${this.calculateTotalCost().toFixed(2)}`)
.fontSize(24)
.fontWeight(FontWeight.Bold)
.fontColor('#FF6B6B')
}
.width('100%')
.padding(16)
.backgroundColor($r('app.color.card_background'))
.borderRadius(8)
}
/**
* 构建提交按钮
*/
@Builder
buildSubmitButton() {
Button('保存记录')
.width('100%')
.height(48)
.margin(16)
.backgroundColor($r('app.color.primary_professional'))
.fontColor(Color.White)
.borderRadius(24)
.onClick(() => {
this.submitRecord();
})
}
第六部分:辅助方法和提交逻辑
typescript
/**
* 获取当前分类的项目选项
*/
private getItemOptions(): string[] {
if (!this.selectedCategory) {
return ['其他'];
}
return this.itemOptions[this.selectedCategory] || ['其他'];
}
/**
* 获取当前分类的单位选项
*/
private getUnitOptions(): string[] {
if (!this.selectedCategory) {
return ['个'];
}
return this.unitOptions[this.selectedCategory] || ['个'];
}
/**
* 计算总成本
*/
private calculateTotalCost(): number {
const qty = parseFloat(this.quantity) || 0;
const price = parseFloat(this.unitPrice) || 0;
return qty * price;
}
/**
* 获取分类图标
*/
private getCategoryIcon(category: string): string {
const iconMap: Record<string, string> = {
'种子': '🌱',
'肥料': '🌿',
'农药': '🛡️',
'人工': '👨🌾',
'机械': '🚜',
'水电': '💧',
'其他': '📋'
};
return iconMap[category] || '💰';
}
/**
* 提交成本记录
*/
private async submitRecord(): Promise<void> {
// 验证必填字段
if (!this.selectedFieldId) {
promptAction.showToast({
message: '请选择地块',
duration: 2000
});
return;
}
if (!this.selectedCategory) {
promptAction.showToast({
message: '请选择成本类别',
duration: 2000
});
return;
}
if (!this.item.trim()) {
promptAction.showToast({
message: '请输入项目名称',
duration: 2000
});
return;
}
const totalCost = this.calculateTotalCost();
if (totalCost <= 0) {
promptAction.showToast({
message: '请输入有效的数量和单价',
duration: 2000
});
return;
}
// 创建成本记录
const record: CostAccountingRecord = {
id: this.costService.generateRecordId(),
fieldId: this.selectedFieldId,
fieldName: this.selectedFieldName,
category: this.selectedCategory,
item: this.item.trim(),
quantity: parseFloat(this.quantity) || 0,
unit: this.unit,
unitPrice: parseFloat(this.unitPrice) || 0,
totalCost: totalCost,
date: Date.now(),
supplier: this.supplier.trim() || undefined,
notes: this.notes.trim() || undefined,
createdAt: Date.now()
};
const success = await this.costService.addRecord(record);
if (success) {
promptAction.showToast({
message: '成本记录添加成功',
duration: 2000
});
router.back();
} else {
promptAction.showToast({
message: '保存失败,请重试',
duration: 2000
});
}
}
}
4.3 表单设计要点
| 设计要点 | 实现说明 |
|---|---|
| 级联选择 | 选择分类后,项目和单位选项自动更新 |
| 自动计算 | 数量×单价实时计算总成本 |
| 必填验证 | 地块、类别、项目、数量、单价为必填项 |
| 下拉选择 | 地块、分类、项目、单位都采用下拉选择,减少输入错误 |
| 智能提示 | 未选择分类时,点击项目或单位会提示先选择分类 |
五、创建编辑成本记录页面
编辑页面与添加页面功能类似,主要区别是需要加载现有数据。
5.1 创建页面文件
文件路径 :entry/src/main/ets/pages/Services/EditCostRecordPage.ets
实现要点:
- 使用
router.getParams()获取传入的成本记录 - 在
aboutToAppear()中加载记录数据到表单 - 提交时调用
updateRecord()而不是addRecord() - 保持记录的
id和createdAt不变
核心代码片段:
typescript
/**
* 加载记录数据
*/
loadRecordData(): void {
const params = router.getParams() as EditCostParams;
if (params && params.record) {
const record = params.record;
this.recordId = record.id;
this.selectedCategory = record.category;
this.item = record.item;
this.quantity = record.quantity.toString();
this.unit = record.unit;
this.unitPrice = record.unitPrice.toString();
this.supplier = record.supplier || '';
this.notes = record.notes || '';
this.selectedFieldId = record.fieldId;
this.selectedFieldName = record.fieldName;
this.createdAt = record.createdAt;
}
}
/**
* 提交修改
*/
private async submitRecord(): Promise<void> {
// ... 验证代码省略 ...
const record: CostAccountingRecord = {
id: this.recordId, // 使用原来的ID
fieldId: this.selectedFieldId,
fieldName: this.selectedFieldName,
category: this.selectedCategory,
item: this.item.trim(),
quantity: parseFloat(this.quantity) || 0,
unit: this.unit,
unitPrice: parseFloat(this.unitPrice) || 0,
totalCost: this.calculateTotalCost(),
date: Date.now(),
supplier: this.supplier.trim() || undefined,
notes: this.notes.trim() || undefined,
createdAt: this.createdAt // 保持原来的创建时间
};
const success = await this.costService.updateRecord(record);
if (success) {
promptAction.showToast({
message: '成本记录修改成功',
duration: 2000
});
router.back();
} else {
promptAction.showToast({
message: '保存失败,请重试',
duration: 2000
});
}
}
六、配置路由
现在需要配置路由,让页面可以被访问。
6.1 配置页面路由
文件位置 :entry/src/main/resources/base/profile/route_map.json
添加路由配置:
json
{
"routerMap": [
// ... 其他路由 ...
{
"name": "CostAccountingPage",
"pageSourceFile": "src/main/ets/pages/Services/CostAccountingPage.ets",
"buildFunction": "CostAccountingPageBuilder"
},
{
"name": "AddCostRecordPage",
"pageSourceFile": "src/main/ets/pages/Services/AddCostRecordPage.ets",
"buildFunction": "AddCostRecordPageBuilder"
},
{
"name": "EditCostRecordPage",
"pageSourceFile": "src/main/ets/pages/Services/EditCostRecordPage.ets",
"buildFunction": "EditCostRecordPageBuilder"
}
]
}
6.2 在首页添加入口
文件位置 :entry/src/main/ets/pages/ProfessionalAgriculture/FieldMapPage.ets(或其他页面)
添加按钮:
typescript
Button('成本核算')
.onClick(() => {
router.pushUrl({
url: 'pages/Services/CostAccountingPage'
});
})
七、测试与验证
现在让我们测试成本核算功能。
7.1 测试步骤
步骤1:运行应用
bash
# 在DevEco Studio中点击运行按钮
步骤2:进入成本核算页面
- 点击首页的"成本核算"按钮
- 查看空状态提示
步骤3:添加第一条成本记录
- 点击"+ 添加成本记录"按钮
- 选择地块(如:东地块)
- 选择成本类别(如:种子)
- 选择项目名称(如:玉米种子)
- 输入数量:
50,选择单位:斤 - 输入单价:
5.5 - 查看自动计算的总成本:
275.00元 - 可选填写供应商和备注
- 点击"保存记录"按钮
步骤4:查看记录列表
- 返回列表页面,查看新添加的记录
- 检查统计卡片数据是否正确
- 点击"分析"按钮,查看分类占比
步骤5:添加更多记录
- 添加不同分类的成本记录
- 观察分类筛选功能
- 查看成本分析图表
步骤6:测试编辑功能
- 点击某条成本记录
- 修改数量或单价
- 查看总成本自动更新
- 保存修改并返回
步骤7:测试删除功能
- 长按某条成本记录
- 确认删除对话框
- 点击"删除"按钮
- 查看记录是否被删除
7.2 预期效果
| 功能 | 预期效果 |
|---|---|
| 列表页面 | 显示总成本统计,记录按时间倒序排列 |
| 筛选功能 | 点击分类标签,只显示该分类的记录 |
| 分析图表 | 显示各分类成本占比,进度条长度正确 |
| 添加记录 | 表单验证通过,数据保存成功,返回列表 |
| 编辑记录 | 加载现有数据,修改保存成功 |
| 删除记录 | 长按显示确认框,删除后列表更新 |
| 自动计算 | 输入数量和单价后,总成本实时更新 |
八、常见问题与解决方案
问题1:添加记录后列表没有刷新
原因:页面状态没有更新
解决方案:
typescript
// 在onPageShow()生命周期中重新加载数据
onPageShow(): void {
this.loadData();
}
问题2:总成本计算不准确
原因:浮点数精度问题
解决方案:
typescript
// 使用toFixed()保留两位小数
private calculateTotalCost(): number {
const qty = parseFloat(this.quantity) || 0;
const price = parseFloat(this.unitPrice) || 0;
return Math.round(qty * price * 100) / 100; // 精确到分
}
问题3:分类筛选后显示不正确
原因:数组过滤逻辑错误
解决方案:
typescript
private getFilteredCostRecords(): CostAccountingRecord[] {
// 确保"全部"分类返回所有记录
if (this.selectedCategory === '全部') {
return this.costRecords;
}
// 严格匹配分类名称
return this.costRecords.filter(r => r.category === this.selectedCategory);
}
问题4:删除记录时应用崩溃
原因:记录ID不存在
解决方案:
typescript
// 在删除前检查记录是否存在
const recordExists = this.costRecords.some(r => r.id === id);
if (!recordExists) {
console.warn('Record not found:', id);
return;
}
九、总结
通过本教程,我们完成了:
已实现功能
✅ 成本核算数据模型设计
✅ CostAccountingService服务层实现
✅ 成本记录的增删改查
✅ 成本统计与分析
✅ 成本分类筛选
✅ 分类占比可视化
✅ 自动计算总成本
✅ 表单验证
技术要点回顾
| 技术点 | 应用场景 |
|---|---|
| 单例模式 | CostAccountingService确保数据一致性 |
| Map数据结构 | 统计分类成本和月度趋势 |
| 状态管理 | @Local装饰器管理页面状态 |
| 级联选择 | 分类→项目→单位的联动 |
| 表单验证 | 必填项检查和数据合法性验证 |
| 路由导航 | 列表→添加/编辑→列表的页面流转 |
下一步
在第13篇教程中,我们将继续实现销售管理与助手功能,包括:
- 销售记录管理
- 销售统计与分析
- 收入趋势分析
- 收益计算