f2303f7fb8
底部提示:左右留白 + 内边距,浅底圆角块,文案改为最低 0.01、余额上限、3 分钟到店出示。 新增地址:进入页会尝试定位,自动选中当前省市区(失败则保留默认郑州);编辑地址不重定位。
254 lines
8.3 KiB
TypeScript
254 lines
8.3 KiB
TypeScript
import { useEffect, useState } from 'react';
|
|
import { View, Text, Input, Textarea, Switch } from '@tarojs/components';
|
|
import Taro, { useRouter } from '@tarojs/taro';
|
|
import PageShell from '../../components/PageShell';
|
|
import SubPageHeader from '../../components/SubPageHeader';
|
|
import RegionPicker from '../../components/RegionPicker';
|
|
import { buildAddressListUrl, readCheckoutContext } from '../../lib/checkout-nav';
|
|
import {
|
|
DEFAULT_REGION,
|
|
REGION_ALL,
|
|
formatRegion,
|
|
type RegionSelection,
|
|
} from '../../lib/region-data';
|
|
import { normalizePhoneInput, validateMobilePhone } from '../../lib/phone';
|
|
import { getStoredUserPhone, resolveDefaultUserPhone } from '../../lib/user-phone';
|
|
import { resolveUserCity } from '../../lib/user-location';
|
|
import { request, toast, type UserProfile } from '../../lib/api';
|
|
|
|
type AddressForm = {
|
|
receiverName: string;
|
|
phone: string;
|
|
province: string;
|
|
city: string;
|
|
district: string;
|
|
detail: string;
|
|
isDefault: boolean;
|
|
};
|
|
|
|
export default function AddressEditPage() {
|
|
const router = useRouter();
|
|
const id = router.params.id;
|
|
const isEdit = !!id;
|
|
const checkoutCtx = readCheckoutContext(router.params);
|
|
const [pickerOpen, setPickerOpen] = useState(false);
|
|
const [saving, setSaving] = useState(false);
|
|
const [locating, setLocating] = useState(false);
|
|
const [error, setError] = useState('');
|
|
const [form, setForm] = useState<AddressForm>(() => ({
|
|
receiverName: '',
|
|
phone: id ? '' : getStoredUserPhone(),
|
|
province: DEFAULT_REGION.province,
|
|
city: DEFAULT_REGION.city,
|
|
district: DEFAULT_REGION.district,
|
|
detail: '',
|
|
isDefault: true,
|
|
}));
|
|
|
|
useEffect(() => {
|
|
if (id) return;
|
|
request<UserProfile>('/auth/me')
|
|
.then((me) => {
|
|
const phone = resolveDefaultUserPhone(me);
|
|
if (!phone) return;
|
|
setForm((prev) => (prev.phone ? prev : { ...prev, phone }));
|
|
})
|
|
.catch(() => {});
|
|
}, [id]);
|
|
|
|
useEffect(() => {
|
|
if (id) return;
|
|
let cancelled = false;
|
|
setLocating(true);
|
|
void resolveUserCity(true)
|
|
.then((resolved) => {
|
|
if (cancelled) return;
|
|
const district =
|
|
resolved.region.district && resolved.region.district !== REGION_ALL
|
|
? resolved.region.district
|
|
: resolved.district && resolved.district !== REGION_ALL
|
|
? resolved.district
|
|
: DEFAULT_REGION.district;
|
|
setForm((prev) => ({
|
|
...prev,
|
|
province: resolved.region.province || prev.province,
|
|
city: resolved.region.city || prev.city,
|
|
district: district || prev.district,
|
|
}));
|
|
})
|
|
.catch(() => {})
|
|
.finally(() => {
|
|
if (!cancelled) setLocating(false);
|
|
});
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, [id]);
|
|
|
|
useEffect(() => {
|
|
if (!id) return;
|
|
request<Array<Record<string, unknown>>>('/user/addresses').then((list) => {
|
|
const found = list.find((a) => String(a.id) === id);
|
|
if (found) {
|
|
setForm({
|
|
receiverName: String(found.receiverName ?? ''),
|
|
phone: String(found.phone ?? ''),
|
|
province: String(found.province ?? DEFAULT_REGION.province),
|
|
city: String(found.city ?? DEFAULT_REGION.city),
|
|
district: String(found.district ?? DEFAULT_REGION.district),
|
|
detail: String(found.detail ?? ''),
|
|
isDefault: found.isDefault === 1 || found.isDefault === true,
|
|
});
|
|
}
|
|
}).catch(() => {});
|
|
}, [id]);
|
|
|
|
const regionText = formatRegion(form.province, form.city, form.district);
|
|
|
|
function validateForm(): string | null {
|
|
if (!form.receiverName.trim()) return '请输入收货人姓名';
|
|
const phoneCheck = validateMobilePhone(form.phone);
|
|
if (!phoneCheck.ok) return phoneCheck.message ?? '请输入正确的手机号码';
|
|
if (!form.province || !form.city || !form.district) return '请选择所在地区';
|
|
if (!form.detail.trim()) return '请输入详细地址';
|
|
return null;
|
|
}
|
|
|
|
async function save() {
|
|
const validationError = validateForm();
|
|
if (validationError) {
|
|
setError(validationError);
|
|
toast(validationError);
|
|
return;
|
|
}
|
|
|
|
setSaving(true);
|
|
setError('');
|
|
try {
|
|
const payload = {
|
|
receiverName: form.receiverName.trim(),
|
|
phone: form.phone.trim(),
|
|
province: form.province,
|
|
city: form.city,
|
|
district: form.district,
|
|
detail: form.detail.trim(),
|
|
isDefault: form.isDefault,
|
|
};
|
|
if (isEdit && id) {
|
|
await request(`/user/addresses/${id}`, { method: 'PUT', data: payload });
|
|
toast('地址已更新', 'success');
|
|
} else {
|
|
await request('/user/addresses', { method: 'POST', data: payload });
|
|
toast('地址已新增', 'success');
|
|
}
|
|
setTimeout(() => {
|
|
Taro.redirectTo({ url: buildAddressListUrl(checkoutCtx) }).catch(() => {
|
|
Taro.navigateBack();
|
|
});
|
|
}, 400);
|
|
} catch (e) {
|
|
const msg = e instanceof Error ? e.message : '保存失败';
|
|
setError(msg);
|
|
toast(msg);
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
}
|
|
|
|
function onRegionConfirm(region: RegionSelection) {
|
|
setForm((prev) => ({
|
|
...prev,
|
|
province: region.province,
|
|
city: region.city,
|
|
district: region.district,
|
|
}));
|
|
}
|
|
|
|
return (
|
|
<PageShell variant="sub" className="address-page" hasFixedFooter>
|
|
<SubPageHeader title={isEdit ? '编辑地址' : '新增地址'} />
|
|
<View className="sub-page-body" style={{ paddingBottom: 80 }}>
|
|
<View className="address-form-field">
|
|
<Text className="address-form-label">收货人</Text>
|
|
<Input
|
|
className="address-form-input"
|
|
placeholder="请输入姓名"
|
|
value={form.receiverName}
|
|
onInput={(e) => setForm((prev) => ({ ...prev, receiverName: e.detail.value }))}
|
|
/>
|
|
</View>
|
|
<View className="address-form-field">
|
|
<Text className="address-form-label">手机号</Text>
|
|
<Input
|
|
className="address-form-input"
|
|
type="number"
|
|
maxlength={11}
|
|
placeholder="请输入手机号"
|
|
value={form.phone}
|
|
onInput={(e) =>
|
|
setForm((prev) => ({ ...prev, phone: normalizePhoneInput(e.detail.value) }))
|
|
}
|
|
/>
|
|
</View>
|
|
<View className="address-form-field">
|
|
<Text className="address-form-label">所在地区</Text>
|
|
<View
|
|
className="address-form-input"
|
|
style={{ display: 'flex', alignItems: 'center' }}
|
|
onClick={() => setPickerOpen(true)}
|
|
>
|
|
<Text>
|
|
{locating && !isEdit
|
|
? '定位中…'
|
|
: regionText || '请选择省市区'}
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
<View className="address-form-field">
|
|
<Text className="address-form-label">详细地址</Text>
|
|
{process.env.TARO_ENV === 'h5' ? (
|
|
<textarea
|
|
className="address-form-textarea address-form-textarea--native"
|
|
placeholder="街道门牌号等"
|
|
rows={3}
|
|
value={form.detail}
|
|
onChange={(e) => {
|
|
const value = e.currentTarget.value;
|
|
setForm((prev) => ({ ...prev, detail: value }));
|
|
}}
|
|
/>
|
|
) : (
|
|
<Textarea
|
|
className="address-form-textarea"
|
|
placeholder="街道门牌号等"
|
|
value={form.detail}
|
|
maxlength={200}
|
|
onInput={(e) => setForm((prev) => ({ ...prev, detail: e.detail.value }))}
|
|
/>
|
|
)}
|
|
</View>
|
|
<View className="address-form-row">
|
|
<Text>设为默认地址</Text>
|
|
<Switch
|
|
checked={form.isDefault}
|
|
color="#A61D24"
|
|
onChange={(e) => setForm((prev) => ({ ...prev, isDefault: e.detail.value }))}
|
|
/>
|
|
</View>
|
|
{error ? <Text className="address-form-error">{error}</Text> : null}
|
|
</View>
|
|
<View className="address-fab" onClick={() => !saving && void save()}>
|
|
<Text>{saving ? '保存中…' : '保存'}</Text>
|
|
</View>
|
|
|
|
<RegionPicker
|
|
open={pickerOpen}
|
|
value={{ province: form.province, city: form.city, district: form.district }}
|
|
onClose={() => setPickerOpen(false)}
|
|
onConfirm={onRegionConfirm}
|
|
levels={3}
|
|
/>
|
|
</PageShell>
|
|
);
|
|
}
|