16.1 地址表单编写

16.1 地址表单编写

img

实现步骤:


第 1 步:i18n

lib/common/i18n/locale_keys.dart

1
2
3
4
5
6
7
8
9
10
11
12
13
// 订单配送地址
static const addressViewTitle = "address_view_title";
static const addressFirstName = "address_first_name";
static const addressLastName = "address_last_name";
static const addressCountry = "address_country";
static const addressState = "address_state";
static const addressPostCode = "address_post_code";
static const addressCity = "address_city";
static const addressAddress1 = "address_address1";
static const addressAddress2 = "address_address2";
static const addressCompany = "address_company";
static const addressPhoneNumber = "address_phone_number";
static const addressEmail = "address_email";

lib/common/i18n/locales/locale_en.dart

1
2
3
4
5
6
7
8
9
10
11
12
13
// 订单配送地址
LocaleKeys.addressViewTitle: '@type address',
LocaleKeys.addressFirstName: 'First name',
LocaleKeys.addressLastName: 'Last name',
LocaleKeys.addressCountry: 'Country',
LocaleKeys.addressState: 'State',
LocaleKeys.addressPostCode: 'Post code',
LocaleKeys.addressCity: 'City',
LocaleKeys.addressAddress1: 'Address 1',
LocaleKeys.addressAddress2: 'Address 2',
LocaleKeys.addressCompany: 'Company',
LocaleKeys.addressPhoneNumber: 'Phone number',
LocaleKeys.addressEmail: 'Email',

lib/common/i18n/locales/locale_zh.dart

1
2
3
4
5
6
7
8
9
10
11
12
13
// 订单配送地址
LocaleKeys.addressViewTitle: '@type 地址',
LocaleKeys.addressFirstName: '姓',
LocaleKeys.addressLastName: '名',
LocaleKeys.addressCountry: '国家',
LocaleKeys.addressState: '洲省',
LocaleKeys.addressPostCode: '邮编',
LocaleKeys.addressCity: '城市',
LocaleKeys.addressAddress1: '地址 1',
LocaleKeys.addressAddress2: '地址 2',
LocaleKeys.addressCompany: '国家',
LocaleKeys.addressPhoneNumber: '电话号码',
LocaleKeys.addressEmail: '电子邮件',

第 2 步:创建模型对象

用户资料 json 格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
{
"id": 6,
"date_created": "2022-04-04T21:54:53",
"date_created_gmt": "2022-04-04T13:54:53",
"date_modified": "2022-08-02T14:07:22",
"date_modified_gmt": "2022-08-02T06:07:22",
"email": "ducafecat5@gmail.com",
"first_name": "ducafe",
"last_name": "cat5",
"role": "customer",
"username": "ducafecat5",
"billing": {
"first_name": "ducafe",
"last_name": "cat5",
"company": "ducafecat",
"address_1": "changping 001",
"address_2": "cp 001",
"city": "beijing",
"postcode": "100101",
"country": "CN",
"state": "CN2",
"email": "ducafecat5@gmail.com",
"phone": "110"
},
"shipping": {
"first_name": "ducafe22",
"last_name": "cat:5",
"company": "ducafecat",
"address_1": "sh 9001-1012-09",
"address_2": "sh 9002",
"city": "Shang:",
"postcode": "200126",
"country": "CN",
"state": "CN32",
"phone": ""
},
"is_paying_customer": false,
"avatar_url": "https://secure.gravatar.com/avatar/8b3a29ec6f524eed54bbf360e545fef8?s=96&d=mm&r=g",
"meta_data": [
{
"id": 123,
"key": "wc_last_active",
"value": "1659484800"
},
{
"id": 141,
"key": "mm_sua_attachment_id",
"value": "71"
}
],
"_links": {
"self": [
{
"href": "https://wp.ducafecat.tech/wp-json/wc/v3/customers/6"
}
],
"collection": [
{
"href": "https://wp.ducafecat.tech/wp-json/wc/v3/customers"
}
]
}
}

订单数据 json 格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
{
"id": 77,
"parent_id": 0,
"status": "cancelled",
"currency": "CNY",
"version": "6.4.1",
"prices_include_tax": false,
"date_created": "2022-04-28T10:52:57",
"date_modified": "2022-04-29T19:31:26",
"discount_total": "0.00",
"discount_tax": "0.00",
"shipping_total": "0.00",
"shipping_tax": "0.00",
"cart_tax": "0.00",
"total": "18.00",
"total_tax": "0.00",
"customer_id": 6,
"order_key": "wc_order_kTohHUYQjAijm",
"billing": {
"first_name": "",
"last_name": "",
"company": "",
"address_1": "",
"address_2": "",
"city": "",
"state": "",
"postcode": "",
"country": "",
"email": "ducafecat5@gmail.com",
"phone": ""
},
"shipping": {
"first_name": "",
"last_name": "",
"company": "",
"address_1": "",
"address_2": "",
"city": "",
"state": "",
"postcode": "",
"country": "",
"phone": ""
},
"payment_method": "",
"payment_method_title": "",
"transaction_id": "",
"customer_ip_address": "",
"customer_user_agent": "",
"created_via": "rest-api",
"customer_note": "",
"date_completed": null,
"date_paid": null,
"cart_hash": "",
"number": "77",
"meta_data": [],
"line_items": [
{
"id": 33,
"name": "Beanie",
"product_id": 15,
"variation_id": 0,
"quantity": 2,
"tax_class": "",
"subtotal": "18.00",
"subtotal_tax": "0.00",
"total": "18.00",
"total_tax": "0.00",
"taxes": [],
"meta_data": [],
"sku": "woo-beanie",
"price": 9,
"parent_name": null,
"product": {
"id": 15,
"name": "Beanie",
"slug": "beanie",
"permalink": "https://wp.ducafecat.tech/product/beanie/",
"date_created": "2022-03-31T23:19:37",
"date_created_gmt": "2022-03-31T15:19:37",
"date_modified": "2022-04-18T23:50:20",
"date_modified_gmt": "2022-04-18T15:50:20",
"type": "simple",
"status": "publish",
"featured": true,
"catalog_visibility": "visible",
"description": "<p>Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.</p>\n",
"short_description": "<p>This is a simple product.</p>\n",
"sku": "woo-beanie",
"price": "18",
"regular_price": "20",
"sale_price": "18",
"date_on_sale_from": null,
"date_on_sale_from_gmt": null,
"date_on_sale_to": null,
"date_on_sale_to_gmt": null,
"on_sale": true,
"purchasable": true,
"total_sales": 0,
"virtual": false,
"downloadable": false,
"downloads": [],
"download_limit": 0,
"download_expiry": 0,
"external_url": "",
"button_text": "",
"tax_status": "taxable",
"tax_class": "",
"manage_stock": false,
"stock_quantity": null,
"backorders": "no",
"backorders_allowed": false,
"backordered": false,
"low_stock_amount": null,
"sold_individually": false,
"weight": "1",
"dimensions": {
"length": "12",
"width": "12",
"height": "3"
},
"shipping_required": true,
"shipping_taxable": true,
"shipping_class": "china_shipping",
"shipping_class_id": 41,
"reviews_allowed": true,
"average_rating": "0.00",
"rating_count": 0,
"upsell_ids": [],
"cross_sell_ids": [],
"parent_id": 0,
"purchase_note": "",
"categories": [
{
"id": 29,
"name": "Man",
"slug": "man"
},
{
"id": 30,
"name": "Woman",
"slug": "woman"
},
{
"id": 34,
"name": "Bag",
"slug": "bag"
},
{
"id": 15,
"name": "More",
"slug": "more"
}
],
"tags": [
{
"id": 42,
"name": "beanie",
"slug": "beanie"
}
],
"images": [
{
"id": 44,
"date_created": "2022-04-01T07:20:01",
"date_created_gmt": "2022-03-31T15:20:01",
"date_modified": "2022-04-01T07:20:01",
"date_modified_gmt": "2022-03-31T15:20:01",
"src": "https://ducafecat.oss-cn-beijing.aliyuncs.com/wp-content/uploads/2022/03/beanie-2.jpg",
"name": "beanie-2.jpg",
"alt": ""
}
],
"attributes": [
{
"id": 1,
"name": "Color",
"position": 0,
"visible": true,
"variation": false,
"options": [
"Green",
"Purple"
]
},
{
"id": 2,
"name": "Size",
"position": 1,
"visible": true,
"variation": false,
"options": [
"XL",
"2XL",
"3XL"
]
}
],
"default_attributes": [],
"variations": [],
"grouped_products": [],
"menu_order": 0,
"price_html": "<del aria-hidden=\"true\"><span class=\"woocommerce-Price-amount amount\"><bdi><span class=\"woocommerce-Price-currencySymbol\">&yen;</span>20.00</bdi></span></del> <ins><span class=\"woocommerce-Price-amount amount\"><bdi><span class=\"woocommerce-Price-currencySymbol\">&yen;</span>18.00</bdi></span></ins>",
"related_ids": [
12,
14,
13,
11
],
"meta_data": [
{
"id": 89,
"key": "_original_id",
"value": "48"
},
{
"id": 479,
"key": "_wpcom_is_markdown",
"value": "1"
}
],
"stock_status": "instock",
"has_options": false,
"_links": {
"self": [
{
"href": "https://wp.ducafecat.tech/wp-json/wc/v3/products/15"
}
],
"collection": [
{
"href": "https://wp.ducafecat.tech/wp-json/wc/v3/products"
}
]
}
}
}
],
"tax_lines": [],
"shipping_lines": [
{
"id": 34,
"method_title": "免费配送",
"method_id": "free_shipping",
"instance_id": "",
"total": "0.00",
"total_tax": "0.00",
"taxes": [],
"meta_data": []
}
],
"fee_lines": [],
"coupon_lines": [],
"refunds": [],
"payment_url": "https://wp.ducafecat.tech/checkout/order-pay/77/?pay_for_order=true&key=wc_order_kTohHUYQjAijm",
"date_created_gmt": "2022-04-28T02:52:57",
"date_modified_gmt": "2022-04-29T11:31:26",
"date_completed_gmt": null,
"date_paid_gmt": null,
"currency_symbol": "¥",
"_links": {
"self": [
{
"href": "https://wp.ducafecat.tech/wp-json/wc/v3/orders/77"
}
],
"collection": [
{
"href": "https://wp.ducafecat.tech/wp-json/wc/v3/orders"
}
],
"customer": [
{
"href": "https://wp.ducafecat.tech/wp-json/wc/v3/customers/6"
}
]
}
}

导包

lib/common/models/index.dart

1
2
3
export './woo/order_model/order_model.dart';
export './woo/order_model/billing.dart';
export './woo/order_model/shipping.dart';

第 3 步:保存地址 API

lib/common/api/user.dart

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/// 保存用户 billing address
static Future<UserProfileModel> saveBillingAddress(Billing? req) async {
var res = await WPHttpService.to.put(
'/users/me',
data: {
"billing": req,
},
);
return UserProfileModel.fromJson(res.data);
}

/// 保存用户 shipping address
static Future<UserProfileModel> saveShippingAddress(Shipping? req) async {
var res = await WPHttpService.to.put(
'/users/me',
data: {
"shipping": req,
},
);
return UserProfileModel.fromJson(res.data);
}

第 4 步:控制器

lib/pages/my/my_address/controller.dart

1
2
// 地址类型 Billing 订单发票地址,Shipping 订单收货地址
final String type = Get.arguments['type'] ?? "";
1
2
// 表单 form
GlobalKey formKey = GlobalKey<FormState>();
1
2
3
4
5
6
7
8
9
10
11
12
// 输入框控制器
TextEditingController firstNameController = TextEditingController();
TextEditingController lastNameController = TextEditingController();
TextEditingController postCodeController = TextEditingController();
TextEditingController cityController = TextEditingController();
TextEditingController address1Controller = TextEditingController();
TextEditingController address2Controller = TextEditingController();
TextEditingController companyController = TextEditingController();
TextEditingController phoneNumberController = TextEditingController();
TextEditingController emailController = TextEditingController();
TextEditingController countryController = TextEditingController();
TextEditingController statesController = TextEditingController();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 初始化
Future<void> _initData() async {
// 用户数据初始
UserProfileModel profile = UserService.to.profile;
if (type == "Billing") {
firstNameController.text = profile.billing?.firstName ?? "";
lastNameController.text = profile.billing?.lastName ?? "";
postCodeController.text = profile.billing?.postcode ?? "";
cityController.text = profile.billing?.city ?? "";
address1Controller.text = profile.billing?.address1 ?? "";
address2Controller.text = profile.billing?.address2 ?? "";
companyController.text = profile.billing?.company ?? "";
phoneNumberController.text = profile.billing?.phone ?? "";
emailController.text = profile.billing?.email ?? "";
countryController.text = profile.billing?.country ?? "";
statesController.text = profile.billing?.state ?? "";
} else {
firstNameController.text = profile.shipping?.firstName ?? "";
lastNameController.text = profile.shipping?.lastName ?? "";
postCodeController.text = profile.shipping?.postcode ?? "";
cityController.text = profile.shipping?.city ?? "";
address1Controller.text = profile.shipping?.address1 ?? "";
address2Controller.text = profile.shipping?.address2 ?? "";
companyController.text = profile.shipping?.company ?? "";
countryController.text = profile.shipping?.country ?? "";
statesController.text = profile.shipping?.state ?? "";
}

update(["my_address"]);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// 保存
Future<void> onSave() async {
if ((formKey.currentState as FormState).validate()) {
UserProfileModel? profile;
if (type == "Billing") {
// 设置账单地址
var req = Billing(
email: emailController.text,
phone: phoneNumberController.text,
firstName: firstNameController.text,
lastName: lastNameController.text,
postcode: postCodeController.text,
city: cityController.text,
address1: address1Controller.text,
address2: address2Controller.text,
company: companyController.text,
country: countryController.text,
state: statesController.text,
);
profile = await UserApi.saveBillingAddress(req);
} else if (type == "Shipping") {
// 设置送货地址
var req = Shipping(
firstName: firstNameController.text,
lastName: lastNameController.text,
postcode: postCodeController.text,
city: cityController.text,
address1: address1Controller.text,
address2: address2Controller.text,
company: companyController.text,
country: countryController.text,
state: statesController.text,
);
profile = await UserApi.saveShippingAddress(req);
}
if (profile != null) {
UserService.to.setProfile(profile);
Get.back<bool>(result: true);
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@override
void onClose() {
super.onClose();
// 控制器释放
firstNameController.dispose();
lastNameController.dispose();
postCodeController.dispose();
cityController.dispose();
address1Controller.dispose();
address2Controller.dispose();
companyController.dispose();
phoneNumberController.dispose();
emailController.dispose();
countryController.dispose();
statesController.dispose();
}

第 5 步:视图

lib/pages/my/my_address/view.dart

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
// 表单
Widget _buildForm() {
return Form(
key: controller.formKey, //设置globalKey,用于后面获取FormState
autovalidateMode: AutovalidateMode.onUserInteraction,
child: <Widget>[
// first name
TextFormWidget(
isMustBeEnter: true,
controller: controller.firstNameController,
labelText: LocaleKeys.addressFirstName.tr,
validator: Validatorless.multiple([
Validatorless.required("The field is obligatory"),
Validatorless.min(3,
"Length cannot be less than @size".trParams({"size": "3"})),
Validatorless.max(
18,
"Length cannot be greater than @size"
.trParams({"size": "18"})),
]),
),

// last name
TextFormWidget(
isMustBeEnter: true,
controller: controller.lastNameController,
labelText: LocaleKeys.addressLastName.tr,
validator: Validatorless.multiple([
Validatorless.required("The field is obligatory"),
Validatorless.min(3,
"Length cannot be less than @size".trParams({"size": "3"})),
Validatorless.max(
18,
"Length cannot be greater than @size"
.trParams({"size": "18"})),
]),
),

// Country

// State

// Post Code
TextFormWidget(
isMustBeEnter: true,
controller: controller.postCodeController,
labelText: LocaleKeys.addressPostCode.tr,
validator: Validatorless.multiple([
Validatorless.required("The field is obligatory"),
Validatorless.min(3,
"Length cannot be less than @size".trParams({"size": "3"})),
Validatorless.max(
12,
"Length cannot be greater than @size"
.trParams({"size": "12"})),
]),
),

// City
TextFormWidget(
isMustBeEnter: true,
controller: controller.cityController,
labelText: LocaleKeys.addressCity.tr,
validator: Validatorless.multiple([
Validatorless.required("The field is obligatory"),
]),
),

// Address 1
TextFormWidget(
isMustBeEnter: true,
controller: controller.address1Controller,
labelText: LocaleKeys.addressAddress1.tr,
validator: Validatorless.multiple([
Validatorless.required("The field is obligatory"),
]),
),

// Address 2
TextFormWidget(
controller: controller.address2Controller,
labelText: LocaleKeys.addressAddress2.tr,
),

// Company
TextFormWidget(
controller: controller.companyController,
labelText: LocaleKeys.addressCompany.tr,
),

// Phone Number
if (controller.type == "Billing")
TextFormWidget(
isMustBeEnter: true,
keyboardType: TextInputType.phone,
controller: controller.phoneNumberController,
labelText: LocaleKeys.addressPhoneNumber.tr,
validator: Validatorless.multiple([
Validatorless.required("The field is obligatory"),
Validatorless.min(3,
"Length cannot be less than @size".trParams({"size": "3"})),
Validatorless.max(
12,
"Length cannot be greater than @size"
.trParams({"size": "12"})),
]),
),

// Email
if (controller.type == "Billing")
TextFormWidget(
isMustBeEnter: true,
keyboardType: TextInputType.emailAddress,
controller: controller.emailController,
labelText: LocaleKeys.addressEmail.tr,
validator: Validatorless.multiple([
Validatorless.required("The field is obligatory"),
Validatorless.email(LocaleKeys.validatorEmail.tr),
]),
),

// end
].toColumn());
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 主视图
Widget _buildView() {
return <Widget>[
// 表单
_buildForm().paddingBottom(30.h),

// 保存按钮
ButtonWidget.primary(
LocaleKeys.commonBottomSave.tr,
onTap: controller.onSave,
height: AppSpace.buttonHeight,
),
].toColumn().paddingAll(AppSpace.page);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@override
Widget build(BuildContext context) {
return GetBuilder<MyAddressController>(
init: MyAddressController(),
id: "my_address",
builder: (_) {
return Scaffold(
appBar: AppBar(
title: Text(
LocaleKeys.addressViewTitle.trParams({
"type": controller.type,
}),
),
),
body: SafeArea(
child: SingleChildScrollView(
child: _buildView(),
),
),
);
},
);
}

第 6 步:地址编辑链接

lib/pages/my/my_index/controller.dart

1
2
3
4
// 地址编辑页 type 1 billing 2 shipping
void onToAddress(String type) {
Get.toNamed(RouteNames.myMyAddress, arguments: {"type": type});
}

lib/pages/my/my_index/view.dart

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 按钮列表
Widget _buildButtonsList() {
return <Widget>[

// Billing Address
ButtonItemWidget(
title: LocaleKeys.myBtnBillingAddress.tr,
svgPath: AssetsSvgs.pHomeSvg,
color: "F43284".toColor,
onTap: () => controller.onToAddress("Billing"), // 类型 billing
),

// Billing Address
ButtonItemWidget(
title: LocaleKeys.myBtnShippingAddress.tr,
svgPath: AssetsSvgs.pHomeSvg,
color: "5F84FF".toColor,
onTap: () => controller.onToAddress("Shipping"), // 类型 shipping
),

提交代码到 git