@shopbb/helium 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/dist/components/AddressBookProvider.d.ts +93 -0
  2. package/dist/components/AddressBookProvider.d.ts.map +1 -0
  3. package/dist/components/AddressBookProvider.js +182 -0
  4. package/dist/components/AddressBookProvider.js.map +1 -0
  5. package/dist/components/AddressForm.d.ts +54 -0
  6. package/dist/components/AddressForm.d.ts.map +1 -0
  7. package/dist/components/AddressForm.js +87 -0
  8. package/dist/components/AddressForm.js.map +1 -0
  9. package/dist/components/AddressList.d.ts +35 -0
  10. package/dist/components/AddressList.d.ts.map +1 -0
  11. package/dist/components/AddressList.js +40 -0
  12. package/dist/components/AddressList.js.map +1 -0
  13. package/dist/components/AddressPicker.d.ts +39 -0
  14. package/dist/components/AddressPicker.d.ts.map +1 -0
  15. package/dist/components/AddressPicker.js +74 -0
  16. package/dist/components/AddressPicker.js.map +1 -0
  17. package/dist/components/DiscountComponents.d.ts +66 -0
  18. package/dist/components/DiscountComponents.d.ts.map +1 -0
  19. package/dist/components/DiscountComponents.js +169 -0
  20. package/dist/components/DiscountComponents.js.map +1 -0
  21. package/dist/components/DiscountProvider.d.ts +143 -0
  22. package/dist/components/DiscountProvider.d.ts.map +1 -0
  23. package/dist/components/DiscountProvider.js +317 -0
  24. package/dist/components/DiscountProvider.js.map +1 -0
  25. package/dist/components/index.d.ts +12 -0
  26. package/dist/components/index.d.ts.map +1 -1
  27. package/dist/components/index.js +8 -0
  28. package/dist/components/index.js.map +1 -1
  29. package/package.json +1 -1
  30. package/src/components/AddressBookProvider.tsx +279 -0
  31. package/src/components/AddressForm.tsx +198 -0
  32. package/src/components/AddressList.tsx +110 -0
  33. package/src/components/AddressPicker.tsx +152 -0
  34. package/src/components/DiscountComponents.tsx +369 -0
  35. package/src/components/DiscountProvider.tsx +455 -0
  36. package/src/components/index.ts +62 -0
@@ -0,0 +1,198 @@
1
+ /**
2
+ * <AddressForm>
3
+ *
4
+ * 地址编辑 / 新增表单。**自带**:
5
+ * - 表单 state(useState)
6
+ * - 必填验证
7
+ * - 调 useAddressBook().createAddress / updateAddress
8
+ * - userErrors 显示
9
+ * - loading 态
10
+ *
11
+ * 用法(新增):
12
+ * <AddressForm onSave={() => setCreating(false)} onCancel={...} />
13
+ *
14
+ * 用法(编辑):
15
+ * <AddressForm initial={editingAddress} onSave={...} onCancel={...} />
16
+ *
17
+ * 表单不带样式(用 data-* 钩子或商家自己 className 控制)。
18
+ */
19
+
20
+ import * as React from 'react';
21
+ import { useAddressBook, type Address, type AddressInput, type AddressBookUserError } from './AddressBookProvider';
22
+
23
+ export interface AddressFormProps {
24
+ /** 传 = 编辑模式;不传 = 新增模式 */
25
+ initial?: Address;
26
+ /** 保存成功后触发 */
27
+ onSave?: (address?: Address) => void;
28
+ /** 取消按钮触发 */
29
+ onCancel?: () => void;
30
+ /** 自定义提交按钮文字 */
31
+ submitText?: string;
32
+ cancelText?: string;
33
+ /** 显示"设为默认"复选框,默认 true */
34
+ showDefaultCheckbox?: boolean;
35
+ /** 哪些字段必填,默认 ['firstName','phone','address1','city','province'] */
36
+ requiredFields?: Array<keyof AddressInput>;
37
+ className?: string;
38
+ /** i18n 文案覆盖 */
39
+ i18n?: Partial<AddressFormI18n>;
40
+ }
41
+
42
+ export interface AddressFormI18n {
43
+ firstName: string;
44
+ lastName: string;
45
+ company: string;
46
+ phone: string;
47
+ country: string;
48
+ province: string;
49
+ city: string;
50
+ district: string;
51
+ address1: string;
52
+ address2: string;
53
+ zip: string;
54
+ setDefault: string;
55
+ saving: string;
56
+ }
57
+
58
+ const DEFAULT_I18N: AddressFormI18n = {
59
+ firstName: '收货人',
60
+ lastName: '姓氏',
61
+ company: '公司',
62
+ phone: '手机号',
63
+ country: '国家',
64
+ province: '省',
65
+ city: '市',
66
+ district: '区',
67
+ address1: '详细地址',
68
+ address2: '地址补充',
69
+ zip: '邮编',
70
+ setDefault: '设为默认地址',
71
+ saving: '保存中…',
72
+ };
73
+
74
+ export function AddressForm(props: AddressFormProps) {
75
+ const {
76
+ initial, onSave, onCancel,
77
+ submitText = '保存',
78
+ cancelText = '取消',
79
+ showDefaultCheckbox = true,
80
+ requiredFields = ['firstName', 'phone', 'address1', 'city', 'province'],
81
+ className,
82
+ i18n: i18nOverride,
83
+ } = props;
84
+
85
+ const { createAddress, updateAddress } = useAddressBook();
86
+ const i18n = { ...DEFAULT_I18N, ...i18nOverride };
87
+
88
+ const [form, setForm] = React.useState<AddressInput>({
89
+ firstName: initial?.firstName ?? '',
90
+ lastName: initial?.lastName ?? '',
91
+ company: initial?.company ?? '',
92
+ phone: initial?.phone ?? '',
93
+ country: initial?.country ?? 'China',
94
+ countryCode: initial?.countryCode ?? 'CN',
95
+ province: initial?.province ?? '',
96
+ provinceCode: initial?.provinceCode ?? '',
97
+ city: initial?.city ?? '',
98
+ district: initial?.district ?? '',
99
+ address1: initial?.address1 ?? '',
100
+ address2: initial?.address2 ?? '',
101
+ zip: initial?.zip ?? '',
102
+ });
103
+ const [asDefault, setAsDefault] = React.useState(initial?.isDefault ?? false);
104
+ const [saving, setSaving] = React.useState(false);
105
+ const [errors, setErrors] = React.useState<AddressBookUserError[]>([]);
106
+
107
+ const update = (k: keyof AddressInput, v: string) => setForm((f) => ({ ...f, [k]: v }));
108
+
109
+ const handleSubmit = async (e: React.FormEvent) => {
110
+ e.preventDefault();
111
+ // 客户端必填校验
112
+ const missing = requiredFields.filter((k) => !(form[k] && String(form[k]).trim()));
113
+ if (missing.length > 0) {
114
+ setErrors([{ field: missing as string[], code: 'MISSING_FIELD', message: '请填写完整地址' }]);
115
+ return;
116
+ }
117
+ setSaving(true);
118
+ setErrors([]);
119
+ try {
120
+ const result = initial
121
+ ? await updateAddress(initial.id, form, asDefault)
122
+ : await createAddress(form, asDefault);
123
+ if (result.userErrors.length > 0) {
124
+ setErrors(result.userErrors);
125
+ return;
126
+ }
127
+ onSave?.(result.address);
128
+ } finally {
129
+ setSaving(false);
130
+ }
131
+ };
132
+
133
+ const isRequired = (k: keyof AddressInput) => requiredFields.includes(k);
134
+
135
+ return (
136
+ <form onSubmit={handleSubmit} data-address-form className={className}>
137
+ <div data-row>
138
+ <label>
139
+ <span>{i18n.firstName}{isRequired('firstName') && ' *'}</span>
140
+ <input value={form.firstName} onChange={(e) => update('firstName', e.target.value)} required={isRequired('firstName')} />
141
+ </label>
142
+ <label>
143
+ <span>{i18n.phone}{isRequired('phone') && ' *'}</span>
144
+ <input value={form.phone} onChange={(e) => update('phone', e.target.value)} required={isRequired('phone')} type="tel" />
145
+ </label>
146
+ </div>
147
+ <div data-row data-row-3>
148
+ <label>
149
+ <span>{i18n.province}{isRequired('province') && ' *'}</span>
150
+ <input value={form.province} onChange={(e) => update('province', e.target.value)} required={isRequired('province')} />
151
+ </label>
152
+ <label>
153
+ <span>{i18n.city}{isRequired('city') && ' *'}</span>
154
+ <input value={form.city} onChange={(e) => update('city', e.target.value)} required={isRequired('city')} />
155
+ </label>
156
+ <label>
157
+ <span>{i18n.district}</span>
158
+ <input value={form.district} onChange={(e) => update('district', e.target.value)} />
159
+ </label>
160
+ </div>
161
+ <label>
162
+ <span>{i18n.address1}{isRequired('address1') && ' *'}</span>
163
+ <input value={form.address1} onChange={(e) => update('address1', e.target.value)} required={isRequired('address1')} />
164
+ </label>
165
+ <label>
166
+ <span>{i18n.address2}</span>
167
+ <input value={form.address2} onChange={(e) => update('address2', e.target.value)} />
168
+ </label>
169
+ <div data-row>
170
+ <label>
171
+ <span>{i18n.zip}</span>
172
+ <input value={form.zip} onChange={(e) => update('zip', e.target.value)} />
173
+ </label>
174
+ {showDefaultCheckbox && (
175
+ <label data-checkbox>
176
+ <input type="checkbox" checked={asDefault} onChange={(e) => setAsDefault(e.target.checked)} />
177
+ <span>{i18n.setDefault}</span>
178
+ </label>
179
+ )}
180
+ </div>
181
+ {errors.length > 0 && (
182
+ <div data-form-errors>
183
+ {errors.map((er, i) => (
184
+ <div key={i} data-error>{er.message}</div>
185
+ ))}
186
+ </div>
187
+ )}
188
+ <div data-actions>
189
+ {onCancel && (
190
+ <button type="button" onClick={onCancel} disabled={saving} data-action="cancel">{cancelText}</button>
191
+ )}
192
+ <button type="submit" disabled={saving} data-action="submit">
193
+ {saving ? i18n.saving : submitText}
194
+ </button>
195
+ </div>
196
+ </form>
197
+ );
198
+ }
@@ -0,0 +1,110 @@
1
+ /**
2
+ * <AddressList>
3
+ *
4
+ * 渲染当前买家的地址列表。
5
+ *
6
+ * 用法:
7
+ * <AddressList
8
+ * onEdit={(addr) => openEditModal(addr)}
9
+ * onAdd={() => openCreateModal()}
10
+ * emptyText="还没有保存的地址"
11
+ * />
12
+ */
13
+
14
+ import * as React from 'react';
15
+ import { useAddressBook, type Address } from './AddressBookProvider';
16
+
17
+ export interface AddressListProps {
18
+ /** 编辑按钮点击 */
19
+ onEdit?: (address: Address) => void;
20
+ /** "新增" 按钮点击。不传 = 不显示新增按钮(商家自己渲染外面) */
21
+ onAdd?: () => void;
22
+ /** 空状态文案 */
23
+ emptyText?: React.ReactNode;
24
+ /** 渲染单个地址卡片的覆盖(高级用法) */
25
+ renderItem?: (address: Address, actions: AddressListItemActions) => React.ReactNode;
26
+ /** 整体容器 className */
27
+ className?: string;
28
+ /** 加载中渲染 */
29
+ loadingFallback?: React.ReactNode;
30
+ }
31
+
32
+ export interface AddressListItemActions {
33
+ setDefault: () => Promise<void>;
34
+ remove: () => Promise<void>;
35
+ edit?: () => void;
36
+ }
37
+
38
+ export function AddressList(props: AddressListProps) {
39
+ const {
40
+ onEdit, onAdd, emptyText = '还没有保存的地址',
41
+ renderItem, className, loadingFallback = null,
42
+ } = props;
43
+ const { addresses, status, setDefault, deleteAddress } = useAddressBook();
44
+
45
+ if (status === 'loading' || status === 'unauthenticated') {
46
+ return <>{loadingFallback}</>;
47
+ }
48
+
49
+ if (addresses.length === 0) {
50
+ return (
51
+ <div data-empty className={className}>
52
+ <p>{emptyText}</p>
53
+ {onAdd && (
54
+ <button type="button" onClick={onAdd} data-add-address>
55
+ + 新增地址
56
+ </button>
57
+ )}
58
+ </div>
59
+ );
60
+ }
61
+
62
+ return (
63
+ <div data-address-list className={className}>
64
+ {onAdd && (
65
+ <div data-list-toolbar>
66
+ <span data-muted>{addresses.length} 个地址</span>
67
+ <button type="button" onClick={onAdd} data-add-address>+ 新增地址</button>
68
+ </div>
69
+ )}
70
+ <div data-items>
71
+ {addresses.map((a) => {
72
+ const actions: AddressListItemActions = {
73
+ setDefault: async () => { await setDefault(a.id); },
74
+ remove: async () => { await deleteAddress(a.id); },
75
+ edit: onEdit ? () => onEdit(a) : undefined,
76
+ };
77
+ if (renderItem) return <React.Fragment key={a.id}>{renderItem(a, actions)}</React.Fragment>;
78
+ return <DefaultAddressItem key={a.id} address={a} actions={actions} />;
79
+ })}
80
+ </div>
81
+ </div>
82
+ );
83
+ }
84
+
85
+ // 默认渲染
86
+ function DefaultAddressItem({ address: a, actions }: { address: Address; actions: AddressListItemActions }) {
87
+ return (
88
+ <div data-address-item data-default={a.isDefault ? '' : undefined}>
89
+ <div data-row>
90
+ <div data-name>
91
+ {a.firstName || ''}{a.lastName || ''}
92
+ {a.isDefault && <span data-default-tag>默认</span>}
93
+ </div>
94
+ <div data-actions>
95
+ {!a.isDefault && (
96
+ <button type="button" onClick={actions.setDefault} data-action="default">设为默认</button>
97
+ )}
98
+ {actions.edit && (
99
+ <button type="button" onClick={actions.edit} data-action="edit">编辑</button>
100
+ )}
101
+ <button type="button" onClick={actions.remove} data-action="remove" data-danger>删除</button>
102
+ </div>
103
+ </div>
104
+ <div data-line>{a.phone || ''}</div>
105
+ <div data-line>
106
+ {a.province || ''}{a.city || ''}{a.district || ''}{a.address1 || ''}{a.address2 ? ' ' + a.address2 : ''}
107
+ </div>
108
+ </div>
109
+ );
110
+ }
@@ -0,0 +1,152 @@
1
+ /**
2
+ * <AddressPicker>
3
+ *
4
+ * Checkout 用的地址选择器。**自带**:
5
+ * - 从 useAddressBook() 拿 list
6
+ * - radio 渲染 + 默认选 defaultAddress
7
+ * - 选 "新地址" 时展开新增表单
8
+ *
9
+ * 用法(受控):
10
+ * <AddressPicker
11
+ * value={selectedAddressId}
12
+ * onChange={(id, addr) => setSelectedAddressId(id)}
13
+ * allowNewAddress
14
+ * onUseNewAddress={(addr) => setInlineAddress(addr)}
15
+ * />
16
+ *
17
+ * 用法(非受控):
18
+ * <AddressPicker onSelect={(addr) => ...} />
19
+ * 组件内部 state;选择后回调商家拿值。
20
+ */
21
+
22
+ import * as React from 'react';
23
+ import { useAddressBook, type Address } from './AddressBookProvider';
24
+ import { AddressForm } from './AddressForm';
25
+
26
+ export interface AddressPickerProps {
27
+ /** 受控:当前选中的 address ID */
28
+ value?: string | null;
29
+ /** 受控:选中变化(同时拿到 address 对象) */
30
+ onChange?: (addressId: string | null, address: Address | null) => void;
31
+ /** 非受控:选择时回调(不需要外面 state) */
32
+ onSelect?: (address: Address | null) => void;
33
+ /** 是否允许"使用新地址"选项 */
34
+ allowNewAddress?: boolean;
35
+ /** 选了"新地址"后填表保存的回调(拿到新建好的 Address) */
36
+ onUseNewAddress?: (address: Address) => void;
37
+ /** 空状态:没保存地址时显示什么。默认渲染内嵌 AddressForm */
38
+ emptyFallback?: React.ReactNode;
39
+ className?: string;
40
+ }
41
+
42
+ export function AddressPicker(props: AddressPickerProps) {
43
+ const {
44
+ value, onChange, onSelect,
45
+ allowNewAddress = true,
46
+ onUseNewAddress,
47
+ emptyFallback,
48
+ className,
49
+ } = props;
50
+ const { addresses, defaultAddress, status } = useAddressBook();
51
+
52
+ // 非受控 fallback
53
+ const [internalId, setInternalId] = React.useState<string | null>(null);
54
+ const selectedId = value !== undefined ? value : internalId;
55
+ const setSelectedId = React.useCallback((id: string | null, addr: Address | null) => {
56
+ if (value === undefined) setInternalId(id);
57
+ onChange?.(id, addr);
58
+ onSelect?.(addr);
59
+ }, [value, onChange, onSelect]);
60
+
61
+ const [showNewForm, setShowNewForm] = React.useState(false);
62
+
63
+ // 初次:自动选 default address
64
+ React.useEffect(() => {
65
+ if (selectedId == null && defaultAddress) {
66
+ setSelectedId(defaultAddress.id, defaultAddress);
67
+ } else if (selectedId == null && addresses.length > 0) {
68
+ setSelectedId(addresses[0].id, addresses[0]);
69
+ }
70
+ }, [defaultAddress, addresses, selectedId, setSelectedId]);
71
+
72
+ if (status === 'loading') {
73
+ return <div data-address-picker data-loading>加载地址中…</div>;
74
+ }
75
+
76
+ // 0 个地址:直接展示一个新增表单
77
+ if (addresses.length === 0) {
78
+ if (emptyFallback) return <>{emptyFallback}</>;
79
+ return (
80
+ <div data-address-picker data-empty className={className}>
81
+ <div data-picker-title>填写收货地址</div>
82
+ <AddressForm
83
+ onSave={(addr) => {
84
+ if (addr) {
85
+ setSelectedId(addr.id, addr);
86
+ onUseNewAddress?.(addr);
87
+ }
88
+ }}
89
+ />
90
+ </div>
91
+ );
92
+ }
93
+
94
+ return (
95
+ <div data-address-picker className={className}>
96
+ <div data-picker-title>选择收货地址</div>
97
+ <div data-picker-list>
98
+ {addresses.map((a) => (
99
+ <label key={a.id} data-picker-item data-active={selectedId === a.id ? '' : undefined}>
100
+ <input
101
+ type="radio"
102
+ name="address-picker"
103
+ checked={selectedId === a.id}
104
+ onChange={() => { setSelectedId(a.id, a); setShowNewForm(false); }}
105
+ />
106
+ <div>
107
+ <div data-name>
108
+ {a.firstName || ''}{a.lastName || ''}
109
+ {a.isDefault && <span data-default-tag>默认</span>}
110
+ </div>
111
+ <div data-line>
112
+ {a.phone} · {a.province}{a.city}{a.district || ''}{a.address1 || ''}
113
+ </div>
114
+ </div>
115
+ </label>
116
+ ))}
117
+ {allowNewAddress && (
118
+ <label data-picker-item data-active={showNewForm ? '' : undefined}>
119
+ <input
120
+ type="radio"
121
+ name="address-picker"
122
+ checked={showNewForm}
123
+ onChange={() => { setShowNewForm(true); setSelectedId(null, null); }}
124
+ />
125
+ <div>
126
+ <div data-name>+ 使用新地址</div>
127
+ <div data-line>填写一个新地址(默认保存到地址簿)</div>
128
+ </div>
129
+ </label>
130
+ )}
131
+ </div>
132
+ {showNewForm && allowNewAddress && (
133
+ <div data-new-address-form>
134
+ <AddressForm
135
+ onCancel={() => {
136
+ setShowNewForm(false);
137
+ const fallback = defaultAddress || addresses[0];
138
+ if (fallback) setSelectedId(fallback.id, fallback);
139
+ }}
140
+ onSave={(addr) => {
141
+ if (addr) {
142
+ setShowNewForm(false);
143
+ setSelectedId(addr.id, addr);
144
+ onUseNewAddress?.(addr);
145
+ }
146
+ }}
147
+ />
148
+ </div>
149
+ )}
150
+ </div>
151
+ );
152
+ }