拦截伪区县写入与下单;推单失败挂 fulfillmentHold,HQ 可见超区/推单失败。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -721,7 +721,15 @@ export default function OrdersPage() {
|
||||
render: (_, row) => (
|
||||
<Space size={4} wrap>
|
||||
<span>{row.productName || '—'}</span>
|
||||
{row.fulfillmentHold ? <Tag color="orange">大单</Tag> : null}
|
||||
{row.fulfillmentHold ? (
|
||||
<Tag color="orange">
|
||||
{row.fulfillmentHoldReason === 'LARGE_ORDER_GE_10_BOXES'
|
||||
? '大单'
|
||||
: row.fulfillmentHoldReason === 'COURIER_OUT_OF_SERVICE'
|
||||
? '超区'
|
||||
: '推单失败'}
|
||||
</Tag>
|
||||
) : null}
|
||||
{row.orderType === 'PROXY' || row.isProxyOrder ? (
|
||||
<Tag color="purple">代下单</Tag>
|
||||
) : null}
|
||||
@@ -1023,7 +1031,7 @@ export default function OrdersPage() {
|
||||
) : <span />}
|
||||
<Space size={12} wrap>
|
||||
<Form.Item name="fulfillmentHold" valuePropName="checked" noStyle>
|
||||
<Checkbox>大单拦截</Checkbox>
|
||||
<Checkbox>履约拦截</Checkbox>
|
||||
</Form.Item>
|
||||
<Button
|
||||
onClick={() => {
|
||||
@@ -1133,7 +1141,7 @@ export default function OrdersPage() {
|
||||
{detail.fulfillmentHold ? (
|
||||
<Tag color="orange">
|
||||
{FULFILLMENT_HOLD_REASON_LABELS[detail.fulfillmentHoldReason || ''] ||
|
||||
'大单待确认'}
|
||||
'履约待确认'}
|
||||
</Tag>
|
||||
) : null}
|
||||
{detail.orderType === 'PROXY' || detail.isProxyOrder ? (
|
||||
@@ -1530,10 +1538,16 @@ export default function OrdersPage() {
|
||||
type="warning"
|
||||
showIcon
|
||||
style={{ marginBottom: 12 }}
|
||||
message="大单已拦截自动推小飞侠"
|
||||
message={
|
||||
shipTarget.fulfillmentHoldReason === 'LARGE_ORDER_GE_10_BOXES'
|
||||
? '大单已拦截自动推小飞侠'
|
||||
: shipTarget.fulfillmentHoldReason === 'COURIER_OUT_OF_SERVICE'
|
||||
? '小飞侠超出服务区'
|
||||
: '自动推配送失败'
|
||||
}
|
||||
description={
|
||||
FULFILLMENT_HOLD_REASON_LABELS[shipTarget.fulfillmentHoldReason || ''] ||
|
||||
'≥10箱订单需总部确认:可选仓推小飞侠,或改用快递自配送。'
|
||||
'请确认后:可选仓重推小飞侠,或改用快递自配送。'
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import QRCode from 'qrcode';
|
||||
import PageHeader from '@dukang/shared-ui/PageHeader';
|
||||
import { invokeWechatPay } from '@dukang/weixin-sdk';
|
||||
import { validateShippingAddress } from '@dukang/domain';
|
||||
import ChinaRegionPicker from '../components/ChinaRegionPicker';
|
||||
import { getToken, request } from '../lib/api';
|
||||
import { formatRegionLabel, parseRegionCodes } from '../lib/china-region';
|
||||
@@ -243,6 +244,13 @@ export default function ProxyOrderPage() {
|
||||
if (!allowOnline) return '该商品不支持线上购买';
|
||||
if (!region?.province || !region?.city || !region?.district) return '请选择省市区';
|
||||
if (!addressDetail.trim()) return '请填写详细地址';
|
||||
const shipping = validateShippingAddress({
|
||||
province: region.province,
|
||||
city: region.city,
|
||||
district: region.district,
|
||||
detail: addressDetail,
|
||||
});
|
||||
if (!shipping.ok) return shipping.message || '请完善收货地址';
|
||||
if (!autoReceive) return '配送到址须勾选同意自动收货';
|
||||
} else if (!allowOnSite) {
|
||||
return '该商品不支持现场提货';
|
||||
|
||||
@@ -6,8 +6,10 @@ import {
|
||||
getCitiesForPicker,
|
||||
getDistricts,
|
||||
getDistrictsForPicker,
|
||||
getDistrictsForShippingPicker,
|
||||
getProvincesForPicker,
|
||||
normalizeRegionSelection,
|
||||
normalizeShippingRegionSelection,
|
||||
toCityLevelRegion,
|
||||
type RegionSelection,
|
||||
} from '../lib/region-data';
|
||||
@@ -18,6 +20,8 @@ type RegionPickerProps = {
|
||||
onClose: () => void;
|
||||
onConfirm: (region: RegionSelection) => void;
|
||||
levels?: 2 | 3;
|
||||
/** filter:门店筛选可「全市」;shipping:收货地址禁止伪区县 */
|
||||
mode?: 'filter' | 'shipping';
|
||||
};
|
||||
|
||||
type PickerLevel = 'province' | 'city' | 'district';
|
||||
@@ -28,8 +32,13 @@ const ALL_TABS: Array<{ key: PickerLevel; label: string }> = [
|
||||
{ key: 'district', label: '区县' },
|
||||
];
|
||||
|
||||
function initialTab(value: RegionSelection, levels: 2 | 3): PickerLevel {
|
||||
const normalized = levels === 2 ? toCityLevelRegion(value) : normalizeRegionSelection(value);
|
||||
function initialTab(value: RegionSelection, levels: 2 | 3, mode: 'filter' | 'shipping'): PickerLevel {
|
||||
const normalized =
|
||||
levels === 2
|
||||
? toCityLevelRegion(value)
|
||||
: mode === 'shipping'
|
||||
? normalizeShippingRegionSelection(value)
|
||||
: normalizeRegionSelection(value);
|
||||
if (levels === 2) {
|
||||
return normalized.province && normalized.province !== REGION_ALL ? 'city' : 'province';
|
||||
}
|
||||
@@ -54,24 +63,35 @@ export default function RegionPicker({
|
||||
onClose,
|
||||
onConfirm,
|
||||
levels = 3,
|
||||
mode = 'filter',
|
||||
}: RegionPickerProps) {
|
||||
const [draft, setDraft] = useState<RegionSelection>(value);
|
||||
const [activeTab, setActiveTab] = useState<PickerLevel>('province');
|
||||
|
||||
const tabs = levels === 2 ? ALL_TABS.slice(0, 2) : ALL_TABS;
|
||||
const shipping = mode === 'shipping';
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
const normalized = levels === 2 ? toCityLevelRegion(value) : normalizeRegionSelection(value);
|
||||
const normalized =
|
||||
levels === 2
|
||||
? toCityLevelRegion(value)
|
||||
: shipping
|
||||
? normalizeShippingRegionSelection(value)
|
||||
: normalizeRegionSelection(value);
|
||||
setDraft(normalized);
|
||||
setActiveTab(initialTab(value, levels));
|
||||
}, [open, value, levels]);
|
||||
setActiveTab(initialTab(value, levels, mode));
|
||||
}, [open, value, levels, mode, shipping]);
|
||||
|
||||
const listItems = useMemo(() => {
|
||||
if (activeTab === 'province') return getProvincesForPicker();
|
||||
if (activeTab === 'city') return getCitiesForPicker(draft.province);
|
||||
if (activeTab === 'city') {
|
||||
if (shipping) return getCities(draft.province);
|
||||
return getCitiesForPicker(draft.province);
|
||||
}
|
||||
if (shipping) return getDistrictsForShippingPicker(draft.province, draft.city);
|
||||
return getDistrictsForPicker(draft.province, draft.city);
|
||||
}, [activeTab, draft.province, draft.city]);
|
||||
}, [activeTab, draft.province, draft.city, shipping]);
|
||||
|
||||
const selectedValue =
|
||||
activeTab === 'province' ? draft.province : activeTab === 'city' ? draft.city : draft.district;
|
||||
@@ -79,12 +99,19 @@ export default function RegionPicker({
|
||||
const canConfirm =
|
||||
levels === 2
|
||||
? Boolean(draft.province && draft.city)
|
||||
: Boolean(draft.province && draft.city && draft.district);
|
||||
: shipping
|
||||
? Boolean(
|
||||
draft.province &&
|
||||
draft.city &&
|
||||
draft.district &&
|
||||
draft.district !== REGION_ALL,
|
||||
)
|
||||
: Boolean(draft.province && draft.city && draft.district);
|
||||
|
||||
if (!open) return null;
|
||||
|
||||
function selectProvince(province: string) {
|
||||
if (province === REGION_ALL) {
|
||||
if (!shipping && province === REGION_ALL) {
|
||||
setDraft({ province: REGION_ALL, city: REGION_ALL, district: REGION_ALL });
|
||||
setActiveTab('city');
|
||||
return;
|
||||
@@ -97,12 +124,16 @@ export default function RegionPicker({
|
||||
return;
|
||||
}
|
||||
const nextDistricts = getDistricts(province, city);
|
||||
setDraft({ province, city, district: nextDistricts[0] ?? '' });
|
||||
setDraft({
|
||||
province,
|
||||
city,
|
||||
district: shipping ? '' : (nextDistricts[0] ?? ''),
|
||||
});
|
||||
setActiveTab('city');
|
||||
}
|
||||
|
||||
function selectCity(city: string) {
|
||||
if (city === REGION_ALL) {
|
||||
if (!shipping && city === REGION_ALL) {
|
||||
setDraft({ ...draft, city: REGION_ALL, district: REGION_ALL });
|
||||
if (levels === 3) setActiveTab('district');
|
||||
return;
|
||||
@@ -112,11 +143,16 @@ export default function RegionPicker({
|
||||
return;
|
||||
}
|
||||
const nextDistricts = getDistricts(draft.province, city);
|
||||
setDraft({ ...draft, city, district: nextDistricts[0] ?? '' });
|
||||
setDraft({
|
||||
...draft,
|
||||
city,
|
||||
district: shipping ? '' : (nextDistricts[0] ?? ''),
|
||||
});
|
||||
setActiveTab('district');
|
||||
}
|
||||
|
||||
function selectDistrict(district: string) {
|
||||
if (shipping && district === REGION_ALL) return;
|
||||
setDraft({ ...draft, district });
|
||||
}
|
||||
|
||||
@@ -134,7 +170,13 @@ export default function RegionPicker({
|
||||
|
||||
function handleConfirm() {
|
||||
if (!canConfirm) return;
|
||||
const next = levels === 2 ? toCityLevelRegion(draft) : normalizeRegionSelection(draft);
|
||||
const next =
|
||||
levels === 2
|
||||
? toCityLevelRegion(draft)
|
||||
: shipping
|
||||
? normalizeShippingRegionSelection(draft)
|
||||
: normalizeRegionSelection(draft);
|
||||
if (shipping && (!next.district || next.district === REGION_ALL)) return;
|
||||
onConfirm(next);
|
||||
onClose();
|
||||
}
|
||||
|
||||
@@ -30,6 +30,12 @@ export function getDistrictsForPicker(province: string, city: string): string[]
|
||||
return [REGION_ALL, ...getDistricts(province, city)];
|
||||
}
|
||||
|
||||
/** 收货地址:不含「全市」 */
|
||||
export function getDistrictsForShippingPicker(province: string, city: string): string[] {
|
||||
if (province === REGION_ALL || city === REGION_ALL) return [];
|
||||
return [...getDistricts(province, city)];
|
||||
}
|
||||
|
||||
export function formatRegion(province: string, city: string, district: string): string {
|
||||
if (!province) return '';
|
||||
if (province === REGION_ALL) return REGION_ALL;
|
||||
@@ -96,6 +102,35 @@ export const DEFAULT_REGION: RegionSelection = {
|
||||
district: REGION_ALL,
|
||||
};
|
||||
|
||||
/** 收货地址默认:不预填伪区县,迫使用户选真实区县 */
|
||||
export const DEFAULT_SHIPPING_REGION: RegionSelection = {
|
||||
province: '河南省',
|
||||
city: '郑州市',
|
||||
district: '',
|
||||
};
|
||||
|
||||
/** 收货场景:区县必须在省市区树内且非全市 */
|
||||
export function normalizeShippingRegionSelection(selection: RegionSelection): RegionSelection {
|
||||
const province = PROVINCES.includes(selection.province)
|
||||
? selection.province
|
||||
: DEFAULT_SHIPPING_REGION.province;
|
||||
const cities = getCities(province);
|
||||
const city = cities.includes(selection.city) ? selection.city : (cities[0] ?? DEFAULT_SHIPPING_REGION.city);
|
||||
const districts = getDistricts(province, city);
|
||||
const district =
|
||||
selection.district &&
|
||||
selection.district !== REGION_ALL &&
|
||||
districts.includes(selection.district)
|
||||
? selection.district
|
||||
: '';
|
||||
return { province, city, district };
|
||||
}
|
||||
|
||||
export function isShippingRegionComplete(selection: RegionSelection): boolean {
|
||||
const n = normalizeShippingRegionSelection(selection);
|
||||
return Boolean(n.province && n.city && n.district && n.district !== REGION_ALL);
|
||||
}
|
||||
|
||||
export function regionFromGeo(province: string, city: string, district?: string): RegionSelection {
|
||||
const cityName = city.endsWith('市') ? city : `${city}市`;
|
||||
const provinceInTree = PROVINCES.includes(province) ? province : DEFAULT_REGION.province;
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import {
|
||||
REGION_ALL,
|
||||
isShippingRegionComplete,
|
||||
normalizeShippingRegionSelection,
|
||||
type RegionSelection,
|
||||
} from './region-data';
|
||||
|
||||
export const SHIPPING_DETAIL_MIN_LEN = 8;
|
||||
export const SHIPPING_REGION_REQUIRED_MSG = '请选择具体区县';
|
||||
export const SHIPPING_DETAIL_REQUIRED_MSG = '请填写详细地址';
|
||||
export const SHIPPING_DETAIL_TOO_SHORT_MSG = '请填写更详细的收货地址(含街道门牌)';
|
||||
|
||||
export type ShippingAddressLike = {
|
||||
province?: string | null;
|
||||
city?: string | null;
|
||||
district?: string | null;
|
||||
detail?: string | null;
|
||||
};
|
||||
|
||||
export function isDirtyShippingAddress(addr: ShippingAddressLike): boolean {
|
||||
return !validateClientShippingAddress(addr).ok;
|
||||
}
|
||||
|
||||
export function validateClientShippingAddress(addr: ShippingAddressLike): {
|
||||
ok: boolean;
|
||||
message?: string;
|
||||
} {
|
||||
const province = String(addr.province ?? '').trim();
|
||||
const city = String(addr.city ?? '').trim();
|
||||
const district = String(addr.district ?? '').trim();
|
||||
const detail = String(addr.detail ?? '').trim();
|
||||
|
||||
if (!province || !city) return { ok: false, message: '请选择所在地区' };
|
||||
if (!district || district === REGION_ALL || district === '全部') {
|
||||
return { ok: false, message: SHIPPING_REGION_REQUIRED_MSG };
|
||||
}
|
||||
const regionOk = isShippingRegionComplete({ province, city, district });
|
||||
if (!regionOk) {
|
||||
return { ok: false, message: SHIPPING_REGION_REQUIRED_MSG };
|
||||
}
|
||||
if (!detail) return { ok: false, message: SHIPPING_DETAIL_REQUIRED_MSG };
|
||||
if (detail.length < SHIPPING_DETAIL_MIN_LEN) {
|
||||
return { ok: false, message: SHIPPING_DETAIL_TOO_SHORT_MSG };
|
||||
}
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
export function shippingDetailCityWideHint(detail: string | null | undefined): string | null {
|
||||
const d = String(detail ?? '').trim();
|
||||
if (!d.includes('全市')) return null;
|
||||
if (/路|街|巷|号|大厦|广场|小区|村|镇|乡/.test(d)) return null;
|
||||
return '详细地址含「全市」,建议改为具体街道门牌,以免配送拒单';
|
||||
}
|
||||
|
||||
export function toShippingRegion(addr: ShippingAddressLike): RegionSelection {
|
||||
return normalizeShippingRegionSelection({
|
||||
province: String(addr.province ?? ''),
|
||||
city: String(addr.city ?? ''),
|
||||
district: String(addr.district ?? ''),
|
||||
});
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -38,6 +38,34 @@
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.address-need-fix-tag {
|
||||
margin-left: 8px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
background: rgba(200, 120, 20, 0.12);
|
||||
color: #b36b00;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.address-item--dirty {
|
||||
border: 1px solid rgba(200, 120, 20, 0.35);
|
||||
}
|
||||
|
||||
.address-item-warn {
|
||||
display: block;
|
||||
margin-top: 6px;
|
||||
font-size: 12px;
|
||||
color: #b36b00;
|
||||
}
|
||||
|
||||
.address-form-hint {
|
||||
display: block;
|
||||
margin: 0 var(--space-page) 8px;
|
||||
font-size: 12px;
|
||||
color: #b36b00;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.address-item-detail {
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
|
||||
Reference in New Issue
Block a user