在HarmonyOS 6购物比价或电商类应用中,"收货地址管理"是下单链路的必经环节:用户可查看地址列表、新增/编辑/删除地址、设为默认,新增时支持从系统通讯录拉取姓名电话、从系统位置服务选取省市区及详细地址。常见坑是删除后列表不刷新、Radio默认选中状态错位、跳转回传新地址未合并。

本文将基于官方行业实践说明,用 List + @State数组驱动 + 路由参数回传 + contact/location权限调用示意​ 完整实现一个可商用的地址管理页。


一、需求拆解与数据模型

1. 功能点

  • 地址列表按"默认置顶 + 其余按添加时间倒序"展示

  • 左滑/按钮删除(带确认),删除默认地址自动将第一条置默认

  • 点击"设为默认" → 对应项 isDefault=true,其余 false,列表重排

  • 新增/编辑页填写后 router.back()带回地址对象 → 主页 onPageShow合并入列表

  • 新增页"从通讯录导入"→ 拉 contactpicker(示意);"从位置选取"→ 拉 geoLocationManager(示意)

2. 数据模型

// model/Address.ets
export interface Address {
  id: string;
  name: string;        // 收货人
  phone: string;        // 手机号
  province: string;      // "广东省"
  city: string;          // "深圳市"
  district: string;      // "南山区"
  detail: string;        // "科技南路XX号XX栋"
  isDefault: boolean;
  tag?: string;          // "公司"/"家"/undefined
}

模拟初始数据:

// mock/addressData.ets
import { Address } from '../model/Address';

export const INIT_ADDRESSES: Address[] = [
  {
    id: 'A001', name: '张三', phone: '138****0001',
    province: '广东省', city: '深圳市', district: '南山区',
    detail: '科技南路XX号XX栋', isDefault: true, tag: '家'
  },
  {
    id: 'A002', name: '李四', phone: '139****0002',
    province: '北京市', city: '北京市', district: '海淀区',
    detail: '中关村大街XX号', isDefault: false, tag: '公司'
  }
];

二、地址管理主页(列表 + 增删改默认)

// pages/AddressManagePage.ets
import { router } from '@kit.ArkUI';
import { INIT_ADDRESSES, Address } from '../mock/addressData';
import { promptAction } from '@kit.ArkUI';

const PAGE_ADDR_EDIT = 'pages/AddressEditPage'; // 新增/编辑页URL

@Entry
@Component
struct AddressManagePage {
  // 地址列表(默认置顶排序)
  @State addresses: Address[] = this.sortAddresses(INIT_ADDRESSES);

  // 置顶默认 + 其余按原顺序
  sortAddresses(list: Address[]): Address[] {
    const def = list.filter(a => a.isDefault);
    const rest = list.filter(a => !a.isDefault);
    return [...def, ...rest];
  }

  // 删除
  deleteAddr(id: string) {
    // 简单确认(可用AlertDialog替换)
    this.addresses = this.addresses.filter(a => a.id !== id);
    // 若删掉的是默认且还有地址 → 第一条自动变默认
    if (!this.addresses.some(a => a.isDefault) && this.addresses.length > 0) {
      this.addresses[0].isDefault = true;
    }
    this.addresses = this.sortAddresses(this.addresses);
  }

  // 设为默认
  setDefault(id: string) {
    this.addresses.forEach(a => a.isDefault = (a.id === id));
    this.addresses = this.sortAddresses(this.addresses);
  }

  // 接收编辑页回传的新/修改后的地址
  mergeOrUpdate(addr: Address) {
    const idx = this.addresses.findIndex(a => a.id === addr.id);
    if (idx >= 0) {
      this.addresses[idx] = addr;  // 编辑
    } else {
      this.addresses.unshift(addr); // 新增
    }
    this.addresses = this.sortAddresses(this.addresses);
  }

  // 页面显示时处理路由回参
  onPageShow() {
    const ctx = this.getUIContext().getHostContext!();
    const params = router.getParams() as Record<string, Address> | undefined;
    if (params?.['savedAddress']) {
      this.mergeOrUpdate(params['savedAddress']);
      router.clearParams(); // 消费掉
    }
  }

  build() {
    Column() {
      // 标题栏
      Row {
        Text('收货地址')
          .fontSize(20)
          .fontWeight(FontWeight.Bold)
        Blank()
        Button('+ 新增')
          .height(34)
          .fontSize(13)
          .backgroundColor('#FF5722')
          .borderRadius(17)
          .padding({ horizontal: 14 })
          .onClick(() => {
            router.pushUrl({ url: PAGE_ADDR_EDIT });
          })
      }
      .padding({ horizontal: 16, top: 16, bottom: 10 })

      // 地址列表
      if (this.addresses.length === 0) {
        Column() {
          Text('暂无收货地址')
            .fontSize(14)
            .fontColor('#BBB')
        }
        .layoutWeight(1)
        .justifyContent(FlexAlign.Center)
      } else {
        List() {
          ForEach(this.addresses, (item: Address) => {
            ListItem() {
              Column({ space: 6 }) {
                // 顶部行:姓名+电话+标签+默认徽章
                Row() {
                  Text(item.name).fontSize(16).fontWeight(FontWeight.Medium)
                  Text(item.phone).fontSize(13).fontColor('#888').margin({ left: 8 })
                  if (item.tag) {
                    Text(item.tag)
                      .fontSize(11)
                      .fontColor('#FF5722')
                      .border({ width: 1, color: '#FF5722' })
                      .borderRadius(4)
                      .padding({ horizontal: 6, vertical: 1 })
                      .margin({ left: 8 })
                  }
                  Blank()
                  if (item.isDefault) {
                    Text('默认')
                      .fontSize(11)
                      .fontColor('#4CAF50')
                      .border({ width: 1, color: '#4CAF50' })
                      .borderRadius(4)
                      .padding({ horizontal: 6, vertical: 1 })
                  }
                }

                // 地址详情
                Text(`${item.province}${item.city}${item.district} ${item.detail}`)
                  .fontSize(13)
                  .fontColor('#555')
                  .maxLines(2)
                  .textOverflow({ overflow: TextOverflow.Ellipsis })

                // 操作区
                Row({ space: 20 }) {
                  Text('编辑')
                    .fontSize(12)
                    .fontColor('#1976D2')
                    .onClick(() => {
                      // 传当前地址给编辑页
                      router.pushUrl({
                        url: PAGE_ADDR_EDIT,
                        params: { editAddress: item }
                      });
                    })

                  Text('删除')
                    .fontSize(12)
                    .fontColor('#F44336')
                    .onClick(() => this.deleteAddr(item.id))

                  Blank()

                  Text('设为默认')
                    .fontSize(12)
                    .fontColor(item.isDefault ? '#CCC' : '#FF5722')
                    .enabled(!item.isDefault)
                    .onClick(() => {
                      if (!item.isDefault) this.setDefault(item.id);
                    })
                }
                .margin({ top: 4 })
              }
              .padding(14)
              .backgroundColor(Color.White)
              .borderRadius(12)
              .shadow({ radius: 3, color: 'rgba(0,0,0,0.05)', offsetX: 0, offsetY: 1 })
            }
          }, (item: Address) => item.id)
        }
        .padding(12)
        .layoutWeight(1)
        .edgeEffect(EdgeEffect.Spring)
      }
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F5F6F8')
  }
}

三、新增/编辑页(含通讯录与位置示意)

// pages/AddressEditPage.ets
import { router } from '@kit.ArkUI';
import { Address } from '../mock/addressData';
import { BusinessError } from '@kit.BasicServicesKit';

@Entry
@Component
struct AddressEditPage {
  // 编辑时接收旧数据,新增时生成空
  private editData: Address | undefined = (router.getParams() as any)?.['editAddress'];

  @State id: string = this.editData?.id ?? `ADDR_${Date.now()}`;
  @State name: string = this.editData?.name ?? '';
  @State phone: string = this.editData?.phone ?? '';
  @State province: string = this.editData?.province ?? '';
  @State city: string = this.editData?.city ?? '';
  @State district: string = this.editData?.district ?? '';
  @State detail: string = this.editData?.detail ?? '';
  @State tag: string = this.editData?.tag ?? '';
  @State isDefault: boolean = this.editData?.isDefault ?? false;

  // 保存回传
  save() {
    if (!this.name.trim() || !this.phone.trim() || !this.detail.trim()) {
      this.getUIContext().getPromptAction().showToast({ message: '请填写必填项' });
      return;
    }
    const addr: Address = {
      id: this.id,
      name: this.name.trim(),
      phone: this.phone.trim(),
      province: this.province,
      city: this.city,
      district: this.district,
      detail: this.detail.trim(),
      isDefault: this.isDefault,
      tag: this.tag || undefined
    };
    router.back({ params: { savedAddress: addr } });
  }

  // ===== 示意:从系统通讯录拉姓名+电话 =====
  pickContact() {
    // 真实项目需调用 @ohos.contact.contactPicker.pickContacts()
    // 下面为示意回填
    this.name = '王五(示例)';
    this.phone = '136****1234';
    this.getUIContext().getPromptAction().showToast({ message: '已从通讯录导入' });
  }

  // ===== 示意:从系统位置服务选取省市区 =====
  pickLocation() {
    // 真实项目需调用 geoLocationManager.getAddressesFromLocation() 或系统选择器
    this.province = '上海市';
    this.city = '上海市';
    this.district = '浦东新区';
    this.getUIContext().getPromptAction().showToast({ message: '已选取位置' });
  }

  build() {
    Column({ space: 14 }) {
      Text(this.editData ? '编辑地址' : '新增地址')
        .fontSize(20)
        .fontWeight(FontWeight.Bold)
        .alignSelf(HorizontalAlign.Start)
        .margin({ top: 16, left: 16 })

      // 表单区
      Scroll() {
        Column({ space: 12 }) {
          // 姓名+电话行
          Row({ space: 8 }) {
            TextInput({ placeholder: '收货人姓名', text: this.name })
              .layoutWeight(1)
              .onChange(v => this.name = v)
            TextInput({ placeholder: '手机号', text: this.phone })
              .layoutWeight(1)
              .onChange(v => this.phone = v)
          }

          // 省市区
          Row({ space: 8 }) {
            Text(`${this.province || '省'}/${this.city || '市'}/${this.district || '区'}`)
              .fontSize(13)
              .fontColor(this.province ? '#333' : '#AAA')
              .layoutWeight(1)
            Button('选择位置')
              .height(32)
              .fontSize(12)
              .backgroundColor('#1976D2')
              .borderRadius(16)
              .padding({ horizontal: 10 })
              .onClick(() => this.pickLocation())
          }

          // 详细地址
          TextArea({ placeholder: '详细地址(街道/楼号/门牌)', text: this.detail })
            .onChange(v => this.detail = v)
            .height(70)

          // 标签
          Row({ space: 8 }) {
            this.tagBtn('家', '家');
            this.tagBtn('公司', '公司');
            this.tagBtn('学校', '学校');
          }

          // 设为默认
          Row() {
            Checkbox({ name: 'isDefault', select: this.isDefault })
              .onChange(v => this.isDefault = v)
            Text('设为默认收货地址').fontSize(13).margin({ left: 6 })
          }
          .margin({ top: 8 })

          // 通讯录导入
          Button('从通讯录导入姓名/电话')
            .width('100%')
            .height(38)
            .backgroundColor('#F5F5F5')
            .fontColor('#1976D2')
            .borderRadius(8)
            .onClick(() => this.pickContact())
        }
        .padding(16)
      }
      .layoutWeight(1)

      // 保存按钮
      Button('保存地址')
        .width('90%')
        .height(44)
        .backgroundColor('#FF5722')
        .borderRadius(22)
        .margin({ bottom: 24 })
        .onClick(() => this.save())
    }
    .width('100%')
    .height('100%')
    .backgroundColor(Color.White)
  }

  @Builder
  tagBtn(label: string, val: string) {
    Button(label)
      .height(30)
      .fontSize(12)
      .backgroundColor(this.tag === val ? '#FF5722' : '#F0F0F0')
      .fontColor(this.tag === val ? Color.White : '#666')
      .borderRadius(15)
      .padding({ horizontal: 12 })
      .onClick(() => this.tag = val)
  }
}

权限说明(开发注意)

  • 通讯录导入需 ohos.permission.READ_CONTACTS(需在 module.json5 声明 + 动态申请)

  • 位置选取需 ohos.permission.LOCATION(精确)或 ohos.permission.APPROXIMATELY_LOCATION,并引导用户开启定位


四、避坑指南

问题

原因

修复

删除/新增后列表不刷新

addresses.filter返回新数组未重新赋值或没触发排序

每次变动后 this.addresses = this.sortAddresses(newArr)

默认地址 Radio/徽章错乱

用 index 做默认判断

item.isDefault布尔字段驱动 UI

编辑后回传数据主页不更新

onPageShow未读 router.getParams()

onPageShowrouter.getParams()+ router.clearParams()

ForEach key用 index

列表重排状态错位

key用 item.id(唯一地址ID)

删默认地址后无默认

删除后未检查重选默认

删除后 if(!hasDefault && len>0) addresses[0].isDefault=true


五、总结:地址管理SOP

  1. 列表驱动@State addresses: Address[],增删改均生成新数组 → 重排(sortAddresses默认置顶)

  2. 路由回传:编辑页 router.back({params:{savedAddress}}),管理页 onPageShow合并或更新

  3. 快捷填充:新增页调 Contact Picker / Location Picker 回填字段(需对应权限)

  4. 默认逻辑:设为默认时全清旧默认再置 true;删除默认地址自动将第一条变默认

核心法则:HarmonyOS 6 电商地址管理 = List + @State数组不可变更新 + router params回传合并 + 默认置顶排序,简单可靠、无三方依赖。

©著作权归作者所有,如需转载,请注明出处,否则将追究法律责任。

Logo

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

更多推荐