HarmonyOS智慧农业管理应用开发教程--高高种地--第13篇:销售管理与助手
通过本篇教程,你将学会:完成本教程后,你将拥有完整的农产品销售管理和收益分析功能。在实现销售管理功能之前,我们需要了解销售数据模型的结构。销售助手服务已在中实现。文件位置:(第9-62行)操作说明:模型设计要点:通过成本和销售两个模块,可以完整分析农业生产的投入产出比和利润情况。SalesAssistantService提供了完整的销售管理功能,包括基础CRUD、筛选查询和统计分析。文件位置:基础
第13篇:销售管理与助手
教程目标
通过本篇教程,你将学会:
- 理解销售数据模型设计
- 实现销售记录的增删改查
- 创建销售助手列表页面
- 创建添加销售记录页面
- 实现销售统计与分析功能
- 按时间周期筛选销售数据
完成本教程后,你将拥有完整的农产品销售管理和收益分析功能。
一、销售数据模型
在实现销售管理功能之前,我们需要了解销售数据模型的结构。销售助手服务已在 SalesAssistantService.ets 中实现。
1.1 查看销售数据模型
文件位置:entry/src/main/ets/services/SalesAssistantService.ets(第9-62行)
操作说明:
- 打开
entry/src/main/ets/services/SalesAssistantService.ets - 查看销售相关的接口定义
/**
* 销售记录接口
*/
export interface SalesRecord {
id: string; // 记录唯一标识
cropName: string; // 作物名称
quantity: number; // 销售数量
unit: string; // 单位
unitPrice: number; // 单价
totalRevenue: number; // 总收入
buyer: string; // 买方
date: number; // 销售日期
fieldId: string; // 关联地块ID
fieldName: string; // 地块名称
notes?: string; // 备注(可选)
createdAt: number; // 创建时间
}
/**
* 销售统计信息接口
*/
export interface SalesStatistics {
totalRevenue: number; // 总收入
recordCount: number; // 记录数量
cropBreakdown: CropSales[]; // 作物销售明细
monthlyTrend: MonthlySales[]; // 月度销售趋势
}
/**
* 作物销售接口
*/
export interface CropSales {
cropName: string; // 作物名称
revenue: number; // 该作物总收入
quantity: number; // 累计销售数量
unit: string; // 单位
percentage: number; // 收入占比百分比
}
/**
* 月度销售接口
*/
export interface MonthlySales {
month: string; // 月份(格式:YYYY-MM)
revenue: number; // 该月总收入
}
模型设计要点:
| 设计要点 | 说明 |
|---|---|
| 地块关联 | 通过fieldId和fieldName关联销售来源地块 |
| 自动计算 | totalRevenue = quantity × unitPrice,简化录入 |
| 作物统计 | 统计每种作物的总收入、数量和占比 |
| 时间分析 | 提供月度销售趋势,便于把握销售规律 |
| 买方信息 | 记录买家名称,便于客户管理 |
1.2 销售数据与成本数据的关系
地块管理系统
├── 成本记录: 种植过程中的各项支出
│ └── CostAccountingRecord (总成本统计)
│
└── 销售记录: 农产品销售的各项收入
└── SalesRecord (总收入统计)
收益分析 = 总收入 - 总成本
通过成本和销售两个模块,可以完整分析农业生产的投入产出比和利润情况。
二、销售助手服务
SalesAssistantService提供了完整的销售管理功能,包括基础CRUD、筛选查询和统计分析。
2.1 SalesAssistantService核心方法
文件位置:entry/src/main/ets/services/SalesAssistantService.ets
基础CRUD方法:
/**
* 获取所有销售记录
*/
async getAllRecords(): Promise<SalesRecord[]>
/**
* 添加销售记录
*/
async addRecord(record: SalesRecord): Promise<boolean>
/**
* 更新销售记录
*/
async updateRecord(record: SalesRecord): Promise<boolean>
/**
* 删除销售记录
*/
async deleteRecord(id: string): Promise<boolean>
时间周期筛选方法:
/**
* 根据时间周期获取记录
* @param period 时间周期('全部'|'本月'|'本季度'|'本年')
*/
async getRecordsByPeriod(period: string): Promise<SalesRecord[]>
/**
* 根据地块获取记录
* @param fieldId 地块ID
*/
async getRecordsByField(fieldId: string): Promise<SalesRecord[]>
统计分析方法:
/**
* 获取销售统计信息
* 包含总收入、记录数、作物销售排行、月度趋势
*/
async getStatistics(): Promise<SalesStatistics>
/**
* 获取地块销售统计
* @param fieldId 地块ID
* @returns 该地块的总销售收入
*/
async getFieldSalesStatistics(fieldId: string): Promise<number>
2.2 时间周期筛选实现原理
本月筛选:
const now = Date.now();
const currentDate = new Date(now);
// 获取本月1号0点的时间戳
const monthStart = new Date(currentDate.getFullYear(), currentDate.getMonth(), 1).getTime();
// 筛选大于等于本月1号的记录
return records.filter(r => r.date >= monthStart);
本季度筛选:
// 计算当前是第几季度(0-3)
const quarter = Math.floor(currentDate.getMonth() / 3);
// 获取本季度第一个月的1号0点
const quarterStart = new Date(currentDate.getFullYear(), quarter * 3, 1).getTime();
return records.filter(r => r.date >= quarterStart);
本年筛选:
// 获取本年1月1号0点的时间戳
const yearStart = new Date(currentDate.getFullYear(), 0, 1).getTime();
return records.filter(r => r.date >= yearStart);
2.3 作物销售统计原理
// 使用Map累加每种作物的收入和数量
const cropMap = new Map<string, CropAccumulator>();
for (const record of records) {
const current = cropMap.get(record.cropName);
if (current) {
// 累加收入和数量
current.revenue += record.totalRevenue;
current.quantity += record.quantity;
} else {
// 首次出现,创建新记录
cropMap.set(record.cropName, {
revenue: record.totalRevenue,
quantity: record.quantity,
unit: record.unit
});
}
}
// 计算每种作物的收入占比
const percentage = totalRevenue > 0 ? (revenue / totalRevenue) * 100 : 0;
// 按收入降序排列
cropBreakdown.sort((a, b) => b.revenue - a.revenue);
三、创建销售助手列表页面
现在开始实现销售助手的列表页面,展示销售记录和收入统计。
3.1 创建页面文件
文件路径:entry/src/main/ets/pages/Services/SalesAssistantPage.ets
页面功能:
- 展示总收入统计卡片
- 支持按时间周期筛选记录
- 显示作物销售占比分析
- 长按删除销售记录
- 点击进入编辑页面
3.2 完整页面代码
/**
* 销售助手页面
* 管理农产品销售记录和收入统计
*/
import { router } from '@kit.ArkUI';
import { promptAction } from '@kit.ArkUI';
import { SalesAssistantService, SalesRecord, SalesStatistics, CropSales } from '../../services/SalesAssistantService';
@Entry
@ComponentV2
export struct SalesAssistantPage {
// ===== 状态变量 =====
@Local salesRecords: SalesRecord[] = []; // 销售记录列表
@Local statistics: SalesStatistics | null = null; // 统计数据
@Local totalRevenue: number = 0; // 总收入
@Local selectedPeriod: string = '全部'; // 选中的时间周期
@Local isLoading: boolean = true; // 加载状态
@Local showAnalysis: boolean = false; // 是否显示分析图表
// ===== 服务实例 =====
private salesService: SalesAssistantService = SalesAssistantService.getInstance();
// ===== 时间周期选项 =====
private periodOptions = ['全部', '本月', '本季度', '本年'];
/**
* 生命周期:页面即将出现
*/
aboutToAppear(): void {
this.loadData();
}
/**
* 生命周期:页面显示时刷新数据
*/
onPageShow(): void {
this.loadData();
}
/**
* 加载销售数据
*/
async loadData(): Promise<void> {
try {
this.salesRecords = await this.salesService.getAllRecords();
this.statistics = await this.salesService.getStatistics();
this.totalRevenue = this.statistics.totalRevenue;
this.isLoading = false;
} catch (error) {
console.error('Failed to load sales data:', error);
this.isLoading = false;
}
}
/**
* 页面主体结构
*/
build() {
Column() {
this.buildHeader()
if (this.isLoading) {
this.buildLoading()
} else {
Column() {
this.buildSummaryCards()
if (this.showAnalysis && this.statistics) {
this.buildAnalysis()
}
this.buildPeriodFilter()
this.buildSalesList()
}
.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
buildSummaryCards() {
Column({ space: 12 }) {
Text('总收入')
.fontSize(14)
.fontColor($r('app.color.text_secondary'))
// 总收入金额
Text(`¥${this.totalRevenue.toFixed(2)}`)
.fontSize(32)
.fontWeight(FontWeight.Bold)
.fontColor('#4CAF50') // 绿色表示收入
// 记录数量和作物种类
Row({ space: 16 }) {
Text(`${this.statistics ? this.statistics.recordCount : 0} 条记录`)
.fontSize(14)
.fontColor($r('app.color.text_secondary'))
if (this.statistics && this.statistics.cropBreakdown.length > 0) {
Text(`${this.statistics.cropBreakdown.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.cropBreakdown.length > 0) {
Column({ space: 8 }) {
ForEach(this.statistics.cropBreakdown, (item: CropSales) => {
Row({ space: 12 }) {
// 作物图标
Text('📦')
.fontSize(16)
// 作物名称
Text(item.cropName)
.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('#4CAF50')
.borderRadius(4)
}
.layoutWeight(1)
// 收入金额
Text(`¥${item.revenue.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
buildPeriodFilter() {
Scroll() {
Row({ space: 8 }) {
ForEach(this.periodOptions, (period: string) => {
Text(period)
.fontSize(14)
.fontColor(this.selectedPeriod === period ?
Color.White : $r('app.color.text_primary'))
.padding({ left: 16, right: 16, top: 8, bottom: 8 })
.backgroundColor(this.selectedPeriod === period ?
$r('app.color.primary_professional') : $r('app.color.card_background'))
.borderRadius(16)
.onClick(() => {
this.selectedPeriod = period;
})
})
}
.padding({ left: 16, right: 16 })
}
.scrollable(ScrollDirection.Horizontal)
.scrollBar(BarState.Off)
.width('100%')
.margin({ bottom: 16 })
}
/**
* 构建销售记录列表
*/
@Builder
buildSalesList() {
if (this.getFilteredSalesRecords().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.getFilteredSalesRecords(), (record: SalesRecord) => {
this.buildSalesCard(record)
})
}
.padding({ left: 16, right: 16, bottom: 16 })
}
.layoutWeight(1)
.scrollBar(BarState.Auto)
}
}
/**
* 构建单个销售记录卡片
*/
@Builder
buildSalesCard(record: SalesRecord) {
Column({ space: 12 }) {
// 第一行:作物名称、来源地块、总收入
Row() {
Text('📦')
.fontSize(20)
Column({ space: 4 }) {
Text(record.cropName)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor($r('app.color.text_primary'))
Text(record.fieldName)
.fontSize(12)
.fontColor($r('app.color.text_secondary'))
}
.alignItems(HorizontalAlign.Start)
.margin({ left: 8 })
.layoutWeight(1)
Text(`¥${record.totalRevenue.toFixed(2)}`)
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#4CAF50')
}
.width('100%')
// 第二行:日期、数量单价、买家
Row() {
Text(this.formatDate(record.date))
.fontSize(12)
.fontColor($r('app.color.text_tertiary'))
Text(`${record.quantity} ${record.unit} × ¥${record.unitPrice}`)
.fontSize(12)
.fontColor($r('app.color.text_secondary'))
.margin({ left: 12 })
Blank()
if (record.buyer) {
Text(record.buyer)
.fontSize(12)
.fontColor($r('app.color.primary_professional'))
}
}
.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/EditSalesRecordPage',
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/AddSalesRecordPage'
});
})
}
/**
* 获取筛选后的销售记录
*/
private getFilteredSalesRecords(): SalesRecord[] {
if (this.selectedPeriod === '全部') {
return this.salesRecords;
}
const now = Date.now();
const currentDate = new Date(now);
if (this.selectedPeriod === '本月') {
const monthStart = new Date(currentDate.getFullYear(), currentDate.getMonth(), 1).getTime();
return this.salesRecords.filter(r => r.date >= monthStart);
} else if (this.selectedPeriod === '本季度') {
const quarter = Math.floor(currentDate.getMonth() / 3);
const quarterStart = new Date(currentDate.getFullYear(), quarter * 3, 1).getTime();
return this.salesRecords.filter(r => r.date >= quarterStart);
} else if (this.selectedPeriod === '本年') {
const yearStart = new Date(currentDate.getFullYear(), 0, 1).getTime();
return this.salesRecords.filter(r => r.date >= yearStart);
}
return this.salesRecords;
}
/**
* 格式化日期
*/
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: SalesRecord): void {
promptAction.showDialog({
title: '确认删除',
message: `确定要删除销售记录 "${record.cropName}" 吗?`,
buttons: [
{ text: '取消', color: '#999999' },
{ text: '删除', color: '#FF6B6B' }
]
}).then(async (result) => {
if (result.index === 1) {
const success = await this.salesService.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/AddSalesRecordPage.ets
页面功能:
- 选择来源地块
- 选择作物名称
- 输入数量和单位
- 输入单价
- 自动计算总收入
- 输入买家信息
- 表单验证
4.2 完整页面代码(分段说明)
第一部分:导入和状态定义
/**
* 添加销售记录页面
* 用于添加新的销售记录
*/
import { router } from '@kit.ArkUI';
import { promptAction } from '@kit.ArkUI';
import { FieldService } from '../../services/FieldService';
import { SalesAssistantService, SalesRecord } from '../../services/SalesAssistantService';
import { FieldInfo, FieldType, IrrigationSystem } from '../../models/ProfessionalAgricultureModels';
@Entry
@ComponentV2
struct AddSalesRecordPage {
// ===== 表单状态 =====
@Local cropName: string = ''; // 作物名称
@Local quantity: string = ''; // 数量
@Local unit: string = ''; // 单位
@Local unitPrice: string = ''; // 单价
@Local buyer: string = ''; // 买家
@Local notes: string = ''; // 备注
@Local selectedFieldId: string = ''; // 选中的地块ID
@Local selectedFieldName: string = ''; // 选中的地块名称
// ===== UI状态 =====
@Local fieldList: FieldInfo[] = []; // 地块列表
@Local showFieldSelector: boolean = false; // 是否显示地块选择器
@Local showCropSelector: boolean = false; // 是否显示作物选择器
@Local showUnitSelector: boolean = false; // 是否显示单位选择器
// ===== 服务实例 =====
private fieldService: FieldService = FieldService.getInstance();
private salesService: SalesAssistantService = SalesAssistantService.getInstance();
// ===== 作物选项 =====
private cropOptions: string[] = [
'玉米', '小麦', '水稻', '大豆', '花生', '棉花',
'白菜', '萝卜', '土豆', '番茄', '黄瓜', '茄子',
'苹果', '梨', '桃', '葡萄', '西瓜', '草莓',
'其他'
];
// ===== 单位选项 =====
private unitOptions: 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.buildCropSelector()
this.buildQuantityInput()
this.buildPriceInput()
this.buildBuyerInput()
this.buildNotesInput()
this.buildTotalRevenue()
}
.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 })
}
第二部分:地块和作物选择器
/**
* 构建地块选择器
*/
@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.showCropSelector = false;
this.showUnitSelector = 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%')
}
/**
* 构建作物选择器
*/
@Builder
buildCropSelector() {
Column({ space: 8 }) {
Text('作物名称 *')
.fontSize(14)
.fontColor($r('app.color.text_secondary'))
.width('100%')
Row() {
Text(this.cropName || '请选择作物')
.fontSize(15)
.fontColor(this.cropName ? $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.showCropSelector = !this.showCropSelector;
this.showFieldSelector = false;
this.showUnitSelector = false;
})
if (this.showCropSelector) {
Column() {
ForEach(this.cropOptions, (crop: string) => {
Row() {
Text(crop)
.fontSize(15)
.fontColor($r('app.color.text_primary'))
.layoutWeight(1)
if (this.cropName === crop) {
Text('✓')
.fontSize(16)
.fontColor($r('app.color.primary_professional'))
}
}
.width('100%')
.padding({ left: 12, right: 12, top: 10, bottom: 10 })
.backgroundColor(this.cropName === crop ? '#E3F2FD' : Color.Transparent)
.onClick(() => {
this.cropName = crop;
this.showCropSelector = 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(() => {
this.showUnitSelector = !this.showUnitSelector;
this.showFieldSelector = false;
this.showCropSelector = false;
})
}
// 单位选择列表
if (this.showUnitSelector) {
Column() {
ForEach(this.unitOptions, (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%')
}
/**
* 构建单价输入框
*/
@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
buildBuyerInput() {
Column({ space: 8 }) {
Text('买家')
.fontSize(14)
.fontColor($r('app.color.text_secondary'))
.width('100%')
TextInput({ placeholder: '选填', text: this.buyer })
.width('100%')
.height(48)
.backgroundColor($r('app.color.card_background'))
.borderRadius(8)
.onChange((value: string) => {
this.buyer = 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
buildTotalRevenue() {
Row() {
Text('总收入')
.fontSize(16)
.fontColor($r('app.color.text_primary'))
Blank()
// 自动计算并显示总收入
Text(`¥${this.calculateTotalRevenue().toFixed(2)}`)
.fontSize(24)
.fontWeight(FontWeight.Bold)
.fontColor('#4CAF50')
}
.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();
})
}
第四部分:辅助方法和提交逻辑
/**
* 计算总收入
*/
private calculateTotalRevenue(): number {
const qty = parseFloat(this.quantity) || 0;
const price = parseFloat(this.unitPrice) || 0;
return qty * price;
}
/**
* 提交销售记录
*/
private async submitRecord(): Promise<void> {
// 验证必填字段
if (!this.selectedFieldId) {
promptAction.showToast({
message: '请选择地块',
duration: 2000
});
return;
}
if (!this.cropName) {
promptAction.showToast({
message: '请选择作物',
duration: 2000
});
return;
}
const totalRevenue = this.calculateTotalRevenue();
if (totalRevenue <= 0) {
promptAction.showToast({
message: '请输入有效的数量和单价',
duration: 2000
});
return;
}
if (!this.unit) {
promptAction.showToast({
message: '请选择单位',
duration: 2000
});
return;
}
// 创建销售记录
const record: SalesRecord = {
id: this.salesService.generateRecordId(),
cropName: this.cropName,
quantity: parseFloat(this.quantity) || 0,
unit: this.unit,
unitPrice: parseFloat(this.unitPrice) || 0,
totalRevenue: totalRevenue,
buyer: this.buyer.trim() || '未知买家',
date: Date.now(),
fieldId: this.selectedFieldId,
fieldName: this.selectedFieldName,
notes: this.notes.trim() || undefined,
createdAt: Date.now()
};
const success = await this.salesService.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/EditSalesRecordPage.ets
实现要点:
- 使用
router.getParams()获取传入的销售记录 - 在
aboutToAppear()中加载记录数据到表单 - 提交时调用
updateRecord()而不是addRecord() - 保持记录的
id和createdAt不变
核心代码片段:
/**
* 加载记录数据
*/
loadRecordData(): void {
const params = router.getParams() as EditSalesParams;
if (params && params.record) {
const record = params.record;
this.recordId = record.id;
this.cropName = record.cropName;
this.quantity = record.quantity.toString();
this.unit = record.unit;
this.unitPrice = record.unitPrice.toString();
this.buyer = record.buyer;
this.notes = record.notes || '';
this.selectedFieldId = record.fieldId;
this.selectedFieldName = record.fieldName;
this.createdAt = record.createdAt;
}
}
/**
* 提交修改
*/
private async submitRecord(): Promise<void> {
// ... 验证代码省略 ...
const record: SalesRecord = {
id: this.recordId, // 使用原来的ID
cropName: this.cropName,
quantity: parseFloat(this.quantity) || 0,
unit: this.unit,
unitPrice: parseFloat(this.unitPrice) || 0,
totalRevenue: this.calculateTotalRevenue(),
buyer: this.buyer.trim() || '未知买家',
date: Date.now(),
fieldId: this.selectedFieldId,
fieldName: this.selectedFieldName,
notes: this.notes.trim() || undefined,
createdAt: this.createdAt // 保持原来的创建时间
};
const success = await this.salesService.updateRecord(record);
if (success) {
promptAction.showToast({
message: '销售记录修改成功',
duration: 2000
});
router.back();
} else {
promptAction.showToast({
message: '保存失败,请重试',
duration: 2000
});
}
}
六、收益分析功能扩展
虽然本教程重点讲解销售管理,但结合第12篇的成本核算,可以实现完整的收益分析。
6.1 收益计算公式
/**
* 计算总收益
*/
async calculateProfit(): Promise<number> {
// 获取总收入
const salesStats = await salesService.getStatistics();
const totalRevenue = salesStats.totalRevenue;
// 获取总成本
const costStats = await costService.getStatistics();
const totalCost = costStats.totalCost;
// 计算净收益
const profit = totalRevenue - totalCost;
return profit;
}
/**
* 计算收益率
*/
async calculateProfitRate(): Promise<number> {
const salesStats = await salesService.getStatistics();
const totalRevenue = salesStats.totalRevenue;
const costStats = await costService.getStatistics();
const totalCost = costStats.totalCost;
if (totalCost === 0) {
return 0;
}
// 收益率 = (收入 - 成本) / 成本 × 100%
const profitRate = ((totalRevenue - totalCost) / totalCost) * 100;
return profitRate;
}
6.2 按地块计算收益
/**
* 计算单个地块的收益
*/
async calculateFieldProfit(fieldId: string): Promise<number> {
// 获取地块销售收入
const fieldRevenue = await salesService.getFieldSalesStatistics(fieldId);
// 获取地块成本支出
const fieldCost = await costService.getFieldCostStatistics(fieldId);
// 计算地块净收益
const fieldProfit = fieldRevenue - fieldCost;
return fieldProfit;
}
七、测试与验证
现在让我们测试销售管理功能。
7.1 测试步骤
步骤1:运行应用
# 在DevEco Studio中点击运行按钮
步骤2:进入销售助手页面
- 点击首页的"销售助手"按钮
- 查看空状态提示
步骤3:添加第一条销售记录
- 点击"+ 添加销售记录"按钮
- 选择地块(如:东地块)
- 选择作物名称(如:玉米)
- 输入数量:
1000,选择单位:斤 - 输入单价:
2.5 - 查看自动计算的总收入:
2500.00元 - 可选填写买家和备注
- 点击"保存记录"按钮
步骤4:查看记录列表
- 返回列表页面,查看新添加的记录
- 检查统计卡片数据是否正确
- 点击"分析"按钮,查看作物销售占比
步骤5:测试时间周期筛选
- 点击"本月"标签,查看本月销售记录
- 点击"本季度"标签,查看本季度销售记录
- 点击"本年"标签,查看本年销售记录
- 点击"全部"标签,返回所有记录
步骤6:添加更多记录
- 添加不同作物的销售记录
- 观察作物销售占比变化
- 查看收入排行
步骤7:测试编辑功能
- 点击某条销售记录
- 修改数量或单价
- 查看总收入自动更新
- 保存修改并返回
步骤8:测试删除功能
- 长按某条销售记录
- 确认删除对话框
- 点击"删除"按钮
- 查看记录是否被删除
7.2 预期效果
| 功能 | 预期效果 |
|---|---|
| 列表页面 | 显示总收入统计,记录按时间倒序排列 |
| 时间筛选 | 点击周期标签,正确筛选时间范围内的记录 |
| 分析图表 | 显示各作物销售占比,进度条长度正确 |
| 添加记录 | 表单验证通过,数据保存成功,返回列表 |
| 编辑记录 | 加载现有数据,修改保存成功 |
| 删除记录 | 长按显示确认框,删除后列表更新 |
| 自动计算 | 输入数量和单价后,总收入实时更新 |
八、常见问题与解决方案
问题1:时间周期筛选不准确
原因:时间戳计算错误
解决方案:
// 确保使用0点的时间戳
const monthStart = new Date(currentDate.getFullYear(), currentDate.getMonth(), 1).getTime();
// 注意月份从0开始,季度计算要除以3
const quarter = Math.floor(currentDate.getMonth() / 3);
问题2:作物销售统计数量单位不一致
原因:同一作物使用了不同单位
解决方案:
// 在统计时使用第一次出现的单位
if (!current) {
cropMap.set(record.cropName, {
revenue: record.totalRevenue,
quantity: record.quantity,
unit: record.unit // 保存首次出现的单位
});
}
问题3:买家字段为空导致显示异常
原因:可选字段未处理空值
解决方案:
// 添加记录时设置默认值
buyer: this.buyer.trim() || '未知买家',
// 显示时检查是否存在
if (record.buyer) {
Text(record.buyer)
}
问题4:删除记录后统计数据未更新
原因:删除后没有重新加载统计数据
解决方案:
// 删除成功后重新加载数据
if (success) {
await this.loadData(); // 重新加载包含统计数据
promptAction.showToast({
message: '删除成功',
duration: 2000
});
}
九、总结
通过本教程,我们完成了:
已实现功能
✅ 销售数据模型设计
✅ SalesAssistantService服务层实现
✅ 销售记录的增删改查
✅ 销售统计与分析
✅ 时间周期筛选(本月/本季度/本年)
✅ 作物销售占比可视化
✅ 自动计算总收入
✅ 表单验证
技术要点回顾
| 技术点 | 应用场景 |
|---|---|
| 单例模式 | SalesAssistantService确保数据一致性 |
| Map数据结构 | 统计作物销售和累加数量 |
| 时间计算 | 本月/本季度/本年的时间范围筛选 |
| 状态管理 | @Local装饰器管理页面状态 |
| 表单验证 | 必填项检查和数据合法性验证 |
| 路由导航 | 列表→添加/编辑→列表的页面流转 |
成本与销售对比
| 特性 | 成本核算 | 销售管理 |
|---|---|---|
| 颜色 | 红色(支出) | 绿色(收入) |
| 筛选 | 按分类(种子/肥料等) | 按时间周期(本月/本季度等) |
| 统计 | 分类成本占比 | 作物销售占比 |
| 分析 | 成本结构分析 | 销售趋势分析 |
| 目标 | 控制成本 | 增加收入 |
下一步
在第14篇教程中,我们将实现数据分析与智能决策功能,整合成本和销售数据,提供:
- 综合收益分析
- 产量预测
- 投入产出比计算
- 智能决策建议
更多推荐


所有评论(0)