在这里插入图片描述

对于手办收藏者来说,记录每一次购买的详细信息是非常重要的。购买日期、购买店铺、实际支付价格等数据不仅是收藏档案的一部分,更是日后评估投资回报、分析消费习惯的重要依据。购买记录功能帮助用户完整记录每个手办的购买历程,让收藏管理更加专业和系统化。本文将从列表展示、日期格式化、空状态处理等角度,详细讲解如何在 Flutter for OpenHarmony 项目中实现一个实用的购买记录页面。

一、购买记录的核心价值

在手办收藏场景中,购买记录不仅是简单的流水账,更承载着以下核心功能:

  • 消费统计:了解自己在手办上的总投入,按月、按年统计消费趋势
  • 店铺分析:记录在哪些店铺购买,评估店铺的可靠性和价格优势
  • 价格对比:对比购买价和当前市场价,计算增值或贬值幅度
  • 税务凭证:完整的购买记录可作为个人资产管理的一部分

这些需求决定了购买记录页面需要信息完整、时间清晰、查询便捷

二、页面结构设计

1. 依赖导入

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:intl/intl.dart';
import '../../providers/figure_provider.dart';

关键依赖说明

  • intl:国际化库,用于格式化日期显示,支持多语言环境
  • provider:状态管理,从 FigureProvider 获取手办数据
  • flutter_screenutil:屏幕适配,确保在不同 OpenHarmony 设备上显示一致

2. 无状态组件定义

class PurchaseHistoryPage extends StatelessWidget {
  const PurchaseHistoryPage({super.key});

采用 StatelessWidget 是因为购买记录数据由 Provider 提供,页面本身只负责展示。这种设计保持组件纯粹,符合 Flutter 的最佳实践。

三、数据过滤与空状态处理

1. Scaffold 基础框架

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('购买记录')),

顶部导航栏显示"购买记录"标题,让用户明确当前所在页面。在完整版本中,可以在右侧添加"筛选"按钮,支持按时间范围、店铺、价格区间过滤记录。

2. Consumer 数据监听

      body: Consumer<FigureProvider>(
        builder: (context, provider, child) {
          final purchases = provider.ownedFigures.where((f) => f.purchaseDate != null).toList();

数据过滤逻辑

  • provider.ownedFigures 获取所有已拥有的手办列表
  • .where((f) => f.purchaseDate != null) 过滤出有购买日期的手办
  • .toList() 将过滤结果转换为列表

这样做的原因是:

  • 不是所有手办都有购买记录(如赠送、交换获得的手办)
  • 只显示有明确购买日期的手办,确保数据完整性
  • 避免显示空日期导致页面混乱

3. 空状态处理

          if (purchases.isEmpty) {
            return const Center(child: Text('暂无购买记录'));
          }

空状态设计

  • 当没有购买记录时,显示"暂无购买记录"提示
  • 使用 Center 组件让文本居中显示,符合用户视觉习惯
  • 避免显示空白页面导致用户困惑

在完整版本中,可以添加更友好的空状态设计:

if (purchases.isEmpty) {
  return Center(
    child: Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        Icon(Icons.receipt_long, size: 80, color: Colors.grey),
        SizedBox(height: 16.h),
        Text('暂无购买记录', style: TextStyle(fontSize: 18.sp, color: Colors.grey)),
        SizedBox(height: 8.h),
        Text('添加手办时记录购买信息', style: TextStyle(fontSize: 14.sp, color: Colors.grey)),
      ],
    ),
  );
}

这样用户能更清楚地了解如何添加购买记录。

四、列表渲染核心逻辑

1. ListView.builder 高效渲染

          return ListView.builder(
            padding: EdgeInsets.all(16.w),
            itemCount: purchases.length,

为什么选择 builder 模式?

  • 性能优化:只渲染可见区域的列表项,当购买记录达到几十条甚至上百条时,性能优势明显
  • 动态扩展:购买记录数量不固定,builder 能灵活适应数据变化
  • 内存友好:避免一次性创建大量 Widget 导致内存峰值

padding 设置

  • EdgeInsets.all(16.w) 为整个列表添加 16 像素的内边距
  • 这样第一个和最后一个列表项不会紧贴屏幕边缘,视觉上更舒适

五、购买记录卡片实现

1. Card 卡片布局

            itemBuilder: (context, index) {
              final figure = purchases[index];
              return Card(
                margin: EdgeInsets.only(bottom: 12.h),

Card 组件的优势

  • 自带阴影和圆角,让每条购买记录独立清晰
  • margin: EdgeInsets.only(bottom: 12.h) 只设置底部间距,避免顶部和底部都有间距导致浪费空间
  • 符合 Material Design 规范,用户体验一致

2. Padding 内边距

                child: Padding(
                  padding: EdgeInsets.all(12.w),

在 Card 内部再添加一层 Padding,确保内容不会紧贴卡片边缘。12 像素的内边距经过实测,既能保证内容清晰,又不会显得过于空旷。

3. Row 横向布局

                  child: Row(
                    children: [
                      const Icon(Icons.receipt_long, size: 40),

左侧图标设计

  • 使用 Icons.receipt_long 收据图标,符合购买记录的主题
  • size: 40 设置图标大小为 40 像素,与右侧文本高度匹配
  • 可以根据购买渠道使用不同图标:
    • 线上购买:Icons.shopping_cart
    • 线下购买:Icons.store
    • 预订:Icons.event_available
                      SizedBox(width: 12.w),

在图标和文本之间添加 12 像素的间距,避免内容过于紧凑。

4. Expanded 自适应宽度

                      Expanded(
                        child: Column(
                          crossAxisAlignment: CrossAxisAlignment.start,

Expanded 的作用

  • 让 Column 占据 Row 中除图标外的所有剩余空间
  • crossAxisAlignment: CrossAxisAlignment.start 让文本左对齐,符合阅读习惯
  • 这样即使手办名称很长,也不会溢出屏幕

5. 手办名称显示

                          children: [
                            Text(figure.name, style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16.sp)),

文本样式设计

  • fontWeight: FontWeight.bold 加粗显示,突出手办名称
  • fontSize: 16.sp 使用响应式字体大小,确保在不同设备上可读性一致
  • 实际项目中显示真实名称,如"《Re:Zero 雷姆》1/7 比例手办"
                            SizedBox(height: 4.h),

在每行文本之间添加 4 像素的垂直间距,避免文本过于紧密。

6. 购买日期显示

                            Text('购买日期: ${figure.purchaseDate != null ? DateFormat('yyyy-MM-dd').format(figure.purchaseDate!) : '未知'}'),

日期格式化逻辑

  • 使用三元运算符判断 purchaseDate 是否为空
  • 如果不为空,使用 DateFormat('yyyy-MM-dd').format() 格式化日期
  • 格式为"年-月-日",如"2024-01-26",符合中文用户习惯
  • 如果为空,显示"未知",避免显示 null 导致错误

DateFormat 的优势

  • 支持多种日期格式,如 yyyy年MM月dd日MM/dd/yyyy
  • 支持国际化,可以根据系统语言自动调整格式
  • 处理时区问题,确保日期显示准确

在实际项目中,可以根据需求调整格式:

// 显示相对时间
final daysSincePurchase = DateTime.now().difference(figure.purchaseDate!).inDays;
Text('购买日期: ${daysSincePurchase}天前'),

// 显示中文格式
Text('购买日期: ${DateFormat('yyyy年MM月dd日', 'zh_CN').format(figure.purchaseDate!)}'),

7. 购买店铺显示

                            Text('购买店铺: ${figure.purchaseStore ?? '未记录'}'),

店铺信息处理

  • 使用 ?? 空值合并运算符,如果 purchaseStore 为空,显示"未记录"
  • 这种处理方式比三元运算符更简洁
  • 店铺信息对于评估购买渠道的可靠性很重要

在实际项目中,可以将店铺名称设置为可点击链接:

GestureDetector(
  onTap: () {
    // 跳转到店铺详情页或打开店铺网址
    Get.to(() => StoreDetailPage(storeName: figure.purchaseStore));
  },
  child: Text(
    '购买店铺: ${figure.purchaseStore ?? '未记录'}',
    style: TextStyle(
      color: Colors.blue,
      decoration: TextDecoration.underline,
    ),
  ),
)

8. 价格显示

                            Text('价格: ¥${figure.price}', style: const TextStyle(color: Colors.red)),

价格样式设计

  • 使用红色显示价格,符合中文用户对价格的视觉认知
  • 前缀 ¥ 明确标识货币单位为人民币
  • 使用 const 优化性能,因为 TextStyle(color: Colors.red) 是不变的

在实际项目中,可以对比购买价和当前市场价:

Column(
  crossAxisAlignment: CrossAxisAlignment.start,
  children: [
    Text('购买价: ¥${figure.purchasePrice}', style: TextStyle(color: Colors.grey)),
    Text('当前价: ¥${figure.currentPrice}', style: TextStyle(color: Colors.red)),
    Text(
      '${figure.currentPrice > figure.purchasePrice ? '增值' : '贬值'} ¥${(figure.currentPrice - figure.purchasePrice).abs()}',
      style: TextStyle(
        color: figure.currentPrice > figure.purchasePrice ? Colors.green : Colors.red,
      ),
    ),
  ],
)

这样用户能直观看到投资回报情况。

六、数据模型扩展

为了支持购买记录功能,需要在 Figure 模型中添加相关字段:

class Figure {
  final String id;
  final String name;
  final double price;
  final DateTime? purchaseDate;
  final String? purchaseStore;
  final double? purchasePrice; // 实际购买价格
  final String? purchaseChannel; // 购买渠道:线上、线下、预订
  final String? purchaseNotes; // 购买备注
  final String? invoiceNumber; // 发票号
  
  Figure({
    required this.id,
    required this.name,
    required this.price,
    this.purchaseDate,
    this.purchaseStore,
    this.purchasePrice,
    this.purchaseChannel,
    this.purchaseNotes,
    this.invoiceNumber,
  });
}

这样可以记录更完整的购买信息。

七、高级功能扩展

1. 时间筛选

添加按钮切换显示不同时间段的购买记录:

Row(
  mainAxisAlignment: MainAxisAlignment.spaceEvenly,
  children: [
    FilterChip(
      label: Text('本月'),
      selected: _period == Period.month,
      onSelected: (selected) => setState(() => _period = Period.month),
    ),
    FilterChip(
      label: Text('本年'),
      selected: _period == Period.year,
      onSelected: (selected) => setState(() => _period = Period.year),
    ),
    FilterChip(
      label: Text('全部'),
      selected: _period == Period.all,
      onSelected: (selected) => setState(() => _period = Period.all),
    ),
  ],
)

2. 消费统计

添加统计卡片显示总消费:

Card(
  child: Padding(
    padding: EdgeInsets.all(16.w),
    child: Row(
      mainAxisAlignment: MainAxisAlignment.spaceAround,
      children: [
        Column(
          children: [
            Text('总消费', style: TextStyle(fontSize: 14.sp, color: Colors.grey)),
            Text(${provider.totalPurchaseAmount}', style: TextStyle(fontSize: 20.sp, fontWeight: FontWeight.bold)),
          ],
        ),
        Column(
          children: [
            Text('平均单价', style: TextStyle(fontSize: 14.sp, color: Colors.grey)),
            Text(${provider.averagePurchasePrice}', style: TextStyle(fontSize: 20.sp, fontWeight: FontWeight.bold)),
          ],
        ),
      ],
    ),
  ),
)

3. 导出功能

支持导出购买记录为 CSV 或 Excel:

Future<void> exportPurchaseHistory() async {
  final csv = const ListToCsvConverter().convert([
    ['手办名称', '购买日期', '购买店铺', '价格'],
    ...purchases.map((f) => [
      f.name,
      DateFormat('yyyy-MM-dd').format(f.purchaseDate!),
      f.purchaseStore ?? '未记录',
      f.price.toString(),
    ]),
  ]);
  
  final file = File('${await getApplicationDocumentsDirectory()}/purchase_history.csv');
  await file.writeAsString(csv);
  await Share.shareFiles([file.path], text: '购买记录');
}

八、OpenHarmony 适配要点

1. 日期格式化国际化

确保日期格式适配不同语言环境:

import 'package:intl/date_symbol_data_local.dart';

void main() async {
  await initializeDateFormatting('zh_CN', null);
  runApp(MyApp());
}

// 使用时指定语言
DateFormat('yyyy年MM月dd日', 'zh_CN').format(date)

2. 数据持久化

使用 sqflite 存储购买记录:

Future<void> savePurchaseRecord(Figure figure) async {
  final db = await database;
  await db.insert('purchase_history', {
    'figure_id': figure.id,
    'purchase_date': figure.purchaseDate?.toIso8601String(),
    'purchase_store': figure.purchaseStore,
    'price': figure.price,
  });
}

3. 性能优化

当购买记录数量很大时,使用分页加载:

ScrollController _scrollController = ScrollController();


void initState() {
  super.initState();
  _scrollController.addListener(() {
    if (_scrollController.position.pixels == _scrollController.position.maxScrollExtent) {
      provider.loadMorePurchases();
    }
  });
}

九、实战经验总结

购买记录页面的实现要点:

  • 数据完整性:记录购买日期、店铺、价格等关键信息
  • 空状态处理:友好提示用户如何添加购买记录
  • 日期格式化:使用 intl 库确保日期显示准确
  • 扩展性:预留筛选、统计、导出等高级功能的接口

通过本文的实战讲解,你已经掌握了在 Flutter for OpenHarmony 项目中构建购买记录功能的核心技巧。


欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐