拦截伪区县写入与下单;推单失败挂 fulfillmentHold,HQ 可见超区/推单失败。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -7,12 +7,16 @@ import SubPageHeader from '../../components/SubPageHeader';
|
||||
import RegionPicker from '../../components/RegionPicker';
|
||||
import { buildAddressListUrl, readCheckoutContext } from '../../lib/checkout-nav';
|
||||
import {
|
||||
DEFAULT_REGION,
|
||||
DEFAULT_SHIPPING_REGION,
|
||||
REGION_ALL,
|
||||
formatRegion,
|
||||
type RegionSelection,
|
||||
} from '../../lib/region-data';
|
||||
import { normalizePhoneInput, validateMobilePhone } from '../../lib/phone';
|
||||
import {
|
||||
shippingDetailCityWideHint,
|
||||
validateClientShippingAddress,
|
||||
} from '../../lib/shipping-address';
|
||||
import { getStoredUserPhone, resolveDefaultUserPhone } from '../../lib/user-phone';
|
||||
import { resolveUserCity } from '../../lib/user-location';
|
||||
import { request, toast, type UserProfile } from '../../lib/api';
|
||||
@@ -36,12 +40,13 @@ export default function AddressEditPage() {
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [locating, setLocating] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [hint, setHint] = useState('');
|
||||
const [form, setForm] = useState<AddressForm>(() => ({
|
||||
receiverName: '',
|
||||
phone: id ? '' : getStoredUserPhone(),
|
||||
province: DEFAULT_REGION.province,
|
||||
city: DEFAULT_REGION.city,
|
||||
district: DEFAULT_REGION.district,
|
||||
province: DEFAULT_SHIPPING_REGION.province,
|
||||
city: DEFAULT_SHIPPING_REGION.city,
|
||||
district: DEFAULT_SHIPPING_REGION.district,
|
||||
detail: '',
|
||||
isDefault: true,
|
||||
}));
|
||||
@@ -69,7 +74,7 @@ export default function AddressEditPage() {
|
||||
? resolved.region.district
|
||||
: resolved.district && resolved.district !== REGION_ALL
|
||||
? resolved.district
|
||||
: DEFAULT_REGION.district;
|
||||
: '';
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
province: resolved.region.province || prev.province,
|
||||
@@ -91,12 +96,13 @@ export default function AddressEditPage() {
|
||||
request<Array<Record<string, unknown>>>('/user/addresses').then((list) => {
|
||||
const found = list.find((a) => String(a.id) === id);
|
||||
if (found) {
|
||||
const district = String(found.district ?? '');
|
||||
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),
|
||||
province: String(found.province ?? DEFAULT_SHIPPING_REGION.province),
|
||||
city: String(found.city ?? DEFAULT_SHIPPING_REGION.city),
|
||||
district: district === REGION_ALL ? '' : district,
|
||||
detail: String(found.detail ?? ''),
|
||||
isDefault: found.isDefault === 1 || found.isDefault === true,
|
||||
});
|
||||
@@ -104,14 +110,21 @@ export default function AddressEditPage() {
|
||||
}).catch(() => {});
|
||||
}, [id]);
|
||||
|
||||
const regionText = formatRegion(form.province, form.city, form.district);
|
||||
useEffect(() => {
|
||||
setHint(shippingDetailCityWideHint(form.detail) ?? '');
|
||||
}, [form.detail]);
|
||||
|
||||
const regionText =
|
||||
form.province && form.city && form.district
|
||||
? 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 '请输入详细地址';
|
||||
const shipping = validateClientShippingAddress(form);
|
||||
if (!shipping.ok) return shipping.message ?? '请完善收货地址';
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -201,7 +214,7 @@ export default function AddressEditPage() {
|
||||
<Text>
|
||||
{locating && !isEdit
|
||||
? '定位中…'
|
||||
: regionText || '请选择省市区'}
|
||||
: regionText || '请选择省 / 市 / 区县'}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
@@ -228,6 +241,7 @@ export default function AddressEditPage() {
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
{hint ? <Text className="address-form-hint">{hint}</Text> : null}
|
||||
<View className="address-form-row">
|
||||
<Text>设为默认地址</Text>
|
||||
<Switch
|
||||
@@ -248,6 +262,7 @@ export default function AddressEditPage() {
|
||||
onClose={() => setPickerOpen(false)}
|
||||
onConfirm={onRegionConfirm}
|
||||
levels={3}
|
||||
mode="shipping"
|
||||
/>
|
||||
</PageShell>
|
||||
);
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
buildOrderConfirmUrl,
|
||||
readCheckoutContext,
|
||||
} from '../../lib/checkout-nav';
|
||||
import { isDirtyShippingAddress } from '../../lib/shipping-address';
|
||||
import { request, toast } from '../../lib/api';
|
||||
|
||||
type Address = {
|
||||
@@ -51,6 +52,11 @@ export default function AddressesPage() {
|
||||
|
||||
function selectAddress(addr: Address) {
|
||||
if (!selectMode) return;
|
||||
if (isDirtyShippingAddress(addr)) {
|
||||
toast('该地址缺少具体区县,请先完善');
|
||||
Taro.navigateTo({ url: buildAddressEditUrl(addr.id, checkoutCtx) }).catch(() => {});
|
||||
return;
|
||||
}
|
||||
Taro.redirectTo({
|
||||
url: buildOrderConfirmUrl({
|
||||
productId: checkoutCtx.productId,
|
||||
@@ -76,6 +82,12 @@ export default function AddressesPage() {
|
||||
}
|
||||
}
|
||||
|
||||
const sorted = [...list].sort((a, b) => {
|
||||
const da = isDirtyShippingAddress(a) ? 0 : 1;
|
||||
const db = isDirtyShippingAddress(b) ? 0 : 1;
|
||||
return da - db;
|
||||
});
|
||||
|
||||
return (
|
||||
<PageShell variant="sub" className="address-page" hasFixedFooter>
|
||||
<SubPageHeader title={selectMode ? '选择收货地址' : '地址管理'} />
|
||||
@@ -84,46 +96,55 @@ export default function AddressesPage() {
|
||||
{!loading && list.length === 0 ? (
|
||||
<View className="u-empty">暂无收货地址</View>
|
||||
) : null}
|
||||
{list.map((a) => (
|
||||
<View
|
||||
key={a.id}
|
||||
className="address-item"
|
||||
onClick={() => selectAddress(a)}
|
||||
>
|
||||
<View className="address-item-head">
|
||||
<Text className="address-item-name">{a.receiverName}</Text>
|
||||
<Text className="address-item-phone">{a.phone}</Text>
|
||||
{a.isDefault === 1 || a.isDefault === true ? (
|
||||
<Text className="address-default-tag">默认</Text>
|
||||
{sorted.map((a) => {
|
||||
const dirty = isDirtyShippingAddress(a);
|
||||
return (
|
||||
<View
|
||||
key={a.id}
|
||||
className={`address-item${dirty ? ' address-item--dirty' : ''}`}
|
||||
onClick={() => selectAddress(a)}
|
||||
>
|
||||
<View className="address-item-head">
|
||||
<Text className="address-item-name">{a.receiverName}</Text>
|
||||
<Text className="address-item-phone">{a.phone}</Text>
|
||||
{dirty ? <Text className="address-need-fix-tag">需完善</Text> : null}
|
||||
{a.isDefault === 1 || a.isDefault === true ? (
|
||||
<Text className="address-default-tag">默认</Text>
|
||||
) : null}
|
||||
</View>
|
||||
<Text className="address-item-detail">{formatAddress(a)}</Text>
|
||||
{dirty ? (
|
||||
<Text className="address-item-warn">缺少具体区县,同城配送可能失败,请编辑完善</Text>
|
||||
) : null}
|
||||
{!selectMode || dirty ? (
|
||||
<View className="address-item-actions">
|
||||
<Text
|
||||
className="address-action"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
Taro.navigateTo({ url: buildAddressEditUrl(a.id, checkoutCtx) }).catch((err) => {
|
||||
toast(err instanceof Error ? err.message : '无法打开编辑页');
|
||||
});
|
||||
}}
|
||||
>
|
||||
{dirty ? '去完善' : '编辑'}
|
||||
</Text>
|
||||
{!selectMode ? (
|
||||
<Text
|
||||
className="address-action"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
void removeAddress(a.id);
|
||||
}}
|
||||
>
|
||||
删除
|
||||
</Text>
|
||||
) : null}
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
<Text className="address-item-detail">{formatAddress(a)}</Text>
|
||||
{!selectMode ? (
|
||||
<View className="address-item-actions">
|
||||
<Text
|
||||
className="address-action"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
Taro.navigateTo({ url: buildAddressEditUrl(a.id, checkoutCtx) }).catch((err) => {
|
||||
toast(err instanceof Error ? err.message : '无法打开编辑页');
|
||||
});
|
||||
}}
|
||||
>
|
||||
编辑
|
||||
</Text>
|
||||
<Text
|
||||
className="address-action"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
void removeAddress(a.id);
|
||||
}}
|
||||
>
|
||||
删除
|
||||
</Text>
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
<View
|
||||
className="address-fab"
|
||||
|
||||
@@ -15,6 +15,7 @@ import DeliveryHintHtml from '../../components/DeliveryHintHtml';
|
||||
import { canCrossCity, isCrossCityAddress } from '../../lib/product-fulfillment';
|
||||
import { loadLocalDeliveries, matchLocalDelivery, resolveLocalDeliveryHintHtml } from '../../lib/local-delivery';
|
||||
import { getProductMainImage } from '../../lib/product-images';
|
||||
import { isDirtyShippingAddress } from '../../lib/shipping-address';
|
||||
import BenefitFigure from '../../components/BenefitFigure';
|
||||
import OrderQtyControls from '../../components/OrderQtyControls';
|
||||
|
||||
@@ -177,7 +178,12 @@ export default function OrderConfirmPage() {
|
||||
const isCross =
|
||||
forceCross || preview?.deliveryType === 'CROSS_CITY' || localCross;
|
||||
const crossBlocked = isCross && !allowCross;
|
||||
const addressOk = preview ? preview.addressOk !== false && !crossBlocked : !crossBlocked;
|
||||
const dirtyAddress = !!selectedAddress && isDirtyShippingAddress(selectedAddress);
|
||||
const addressOk = dirtyAddress
|
||||
? false
|
||||
: preview
|
||||
? preview.addressOk !== false && !crossBlocked
|
||||
: !crossBlocked;
|
||||
const unitLabel = preview?.saleUnit === 'BOX' ? '箱' : '瓶';
|
||||
const minQty =
|
||||
preview?.minQty ??
|
||||
@@ -186,9 +192,11 @@ export default function OrderConfirmPage() {
|
||||
const canSubmit =
|
||||
!!addressId && !!preview && quantityOk && addressOk && !loading && !previewLoading;
|
||||
|
||||
const addressHint = !addressOk
|
||||
? preview?.addressMessage || CROSS_CITY_BLOCK_MSG
|
||||
: '';
|
||||
const addressHint = dirtyAddress
|
||||
? '请完善收货地址(需选择具体区县)'
|
||||
: !addressOk
|
||||
? preview?.addressMessage || CROSS_CITY_BLOCK_MSG
|
||||
: '';
|
||||
|
||||
function updateQuantity(next: number) {
|
||||
if (next < minQty) {
|
||||
|
||||
Reference in New Issue
Block a user