89fd333702
Add client-logging SDK, expanded event taxonomy, API observability, admin domain events UI, and move SENTRY_DSN to HQ system settings with @sentry/node bootstrap after config preload. Co-authored-by: Cursor <cursoragent@cursor.com>
264 lines
9.2 KiB
TypeScript
264 lines
9.2 KiB
TypeScript
import { useCallback, useEffect, useState } from 'react';
|
|
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
|
|
import SubPageHeader from '../components/SubPageHeader';
|
|
import { request } from '../lib/api';
|
|
import { useUserSession } from '../contexts/UserSessionContext';
|
|
import { buildAddressEditUrl, buildAddressListUrl, buildOrderConfirmUrl, hasCheckoutContext, readCheckoutContext } from '../lib/navigation';
|
|
import { usePageView } from '../lib/usePageView';
|
|
|
|
type Address = {
|
|
id: string;
|
|
receiverName: string;
|
|
phone: string;
|
|
province: string;
|
|
city: string;
|
|
district: string;
|
|
detail: string;
|
|
isDefault?: number;
|
|
};
|
|
|
|
function maskPhone(phone: string) {
|
|
return phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2');
|
|
}
|
|
|
|
function formatAddress(a: Address) {
|
|
return `${a.province}${a.city}${a.district}${a.detail}`;
|
|
}
|
|
|
|
export default function AddressListPage() {
|
|
usePageView('address_list_view');
|
|
const { profile } = useUserSession();
|
|
const [list, setList] = useState<Address[]>([]);
|
|
const [pendingAddress, setPendingAddress] = useState<Address | null>(null);
|
|
const [savingOrderAddress, setSavingOrderAddress] = useState(false);
|
|
const [params] = useSearchParams();
|
|
const navigate = useNavigate();
|
|
const selectMode = params.get('select') === '1';
|
|
const orderId = params.get('orderId') || '';
|
|
const productId = params.get('productId') || '';
|
|
const qty = params.get('qty') || '';
|
|
const cross = params.get('cross') === '1';
|
|
const currentAddressId = params.get('addressId') || '';
|
|
|
|
const loadList = useCallback(() => {
|
|
request<Address[]>('USER_H5', '/user/addresses').then(setList);
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
loadList();
|
|
}, [loadList, profile?.id]);
|
|
|
|
function selectAddress(addr: Address) {
|
|
if (!selectMode) return;
|
|
if (orderId) {
|
|
setPendingAddress(addr);
|
|
return;
|
|
}
|
|
const qs = new URLSearchParams();
|
|
if (productId) qs.set('productId', productId);
|
|
if (qty) qs.set('qty', qty);
|
|
if (cross) qs.set('cross', '1');
|
|
qs.set('addressId', addr.id);
|
|
navigate(`/order/confirm?${qs.toString()}`);
|
|
}
|
|
|
|
async function confirmOrderAddress() {
|
|
if (!orderId || !pendingAddress) return;
|
|
setSavingOrderAddress(true);
|
|
try {
|
|
await request('USER_H5', `/trade/orders/${orderId}/address`, {
|
|
method: 'PUT',
|
|
body: JSON.stringify({
|
|
receiverName: pendingAddress.receiverName,
|
|
receiverPhone: pendingAddress.phone,
|
|
receiverProvince: pendingAddress.province,
|
|
receiverCity: pendingAddress.city,
|
|
receiverDistrict: pendingAddress.district,
|
|
receiverAddress: `${pendingAddress.province}${pendingAddress.city}${pendingAddress.district}${pendingAddress.detail}`,
|
|
}),
|
|
});
|
|
navigate(`/orders/${orderId}`);
|
|
} catch (e) {
|
|
window.alert(e instanceof Error ? e.message : '修改地址失败');
|
|
} finally {
|
|
setSavingOrderAddress(false);
|
|
setPendingAddress(null);
|
|
}
|
|
}
|
|
|
|
async function setDefault(addr: Address, e: React.MouseEvent) {
|
|
e.stopPropagation();
|
|
if (addr.isDefault === 1) return;
|
|
await request('USER_H5', `/user/addresses/${addr.id}`, {
|
|
method: 'PUT',
|
|
body: JSON.stringify({
|
|
receiverName: addr.receiverName,
|
|
phone: addr.phone,
|
|
province: addr.province,
|
|
city: addr.city,
|
|
district: addr.district,
|
|
detail: addr.detail,
|
|
isDefault: true,
|
|
}),
|
|
});
|
|
loadList();
|
|
}
|
|
|
|
async function removeAddress(id: string, e: React.MouseEvent) {
|
|
e.stopPropagation();
|
|
if (!window.confirm('确定删除该收货地址吗?')) return;
|
|
await request('USER_H5', `/user/addresses/${id}`, { method: 'DELETE' });
|
|
loadList();
|
|
}
|
|
|
|
const checkoutCtx = readCheckoutContext(params);
|
|
|
|
function goEdit(id: string, e: React.MouseEvent) {
|
|
e.stopPropagation();
|
|
navigate(buildAddressEditUrl(id, checkoutCtx));
|
|
}
|
|
|
|
function goBack() {
|
|
if (orderId) {
|
|
navigate(`/orders/${orderId}`);
|
|
return;
|
|
}
|
|
if (hasCheckoutContext(checkoutCtx)) {
|
|
navigate(buildOrderConfirmUrl(checkoutCtx));
|
|
return;
|
|
}
|
|
navigate('/mine');
|
|
}
|
|
|
|
return (
|
|
<div className="address-list-page">
|
|
<SubPageHeader title={orderId ? '选择收货地址' : '我的地址'} onBack={goBack} />
|
|
|
|
<main className="address-list-main sub-page-body">
|
|
{list.length === 0 && (
|
|
<p className="address-list-empty">暂无收货地址,请新增</p>
|
|
)}
|
|
|
|
<div className="address-list-cards">
|
|
{list.map((a) => {
|
|
const isDefault = Number(a.isDefault) === 1;
|
|
const isSelected = selectMode && currentAddressId === String(a.id);
|
|
return (
|
|
<article
|
|
key={a.id}
|
|
className={`address-list-card${isDefault ? ' is-default' : ''}${isSelected ? ' is-selected' : ''}${selectMode ? ' is-selectable' : ''}`}
|
|
onClick={() => selectAddress(a)}
|
|
onKeyDown={(e) => {
|
|
if (e.key === 'Enter' || e.key === ' ') {
|
|
e.preventDefault();
|
|
selectAddress(a);
|
|
}
|
|
}}
|
|
role={selectMode ? 'button' : undefined}
|
|
tabIndex={selectMode ? 0 : undefined}
|
|
>
|
|
<div className="address-list-card-head">
|
|
<div className="address-list-card-contact">
|
|
<span className="address-list-name">{a.receiverName}</span>
|
|
<span className="address-list-phone">{maskPhone(a.phone)}</span>
|
|
</div>
|
|
{isDefault && <span className="address-list-default-badge">默认</span>}
|
|
</div>
|
|
|
|
<p className="address-list-detail">{formatAddress(a)}</p>
|
|
|
|
<div className="address-list-divider" />
|
|
|
|
<div className="address-list-actions">
|
|
{isDefault ? (
|
|
<div className="address-list-default-label">
|
|
<span className="material-symbols-outlined fill-icon">check_circle</span>
|
|
<span>默认地址</span>
|
|
</div>
|
|
) : (
|
|
<button
|
|
type="button"
|
|
className="address-list-set-default"
|
|
onClick={(e) => setDefault(a, e)}
|
|
>
|
|
<span className="material-symbols-outlined">radio_button_unchecked</span>
|
|
<span>设为默认</span>
|
|
</button>
|
|
)}
|
|
|
|
<div className="address-list-action-btns">
|
|
<button type="button" className="address-list-action-btn" onClick={(e) => goEdit(String(a.id), e)}>
|
|
<span className="material-symbols-outlined">edit</span>
|
|
<span>编辑</span>
|
|
</button>
|
|
<button
|
|
type="button"
|
|
className="address-list-action-btn"
|
|
onClick={(e) => removeAddress(String(a.id), e)}
|
|
>
|
|
<span className="material-symbols-outlined">delete</span>
|
|
<span>删除</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</article>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
{list.length > 0 && (
|
|
<div className="address-list-brand" aria-hidden>
|
|
<div className="address-list-brand-icon">
|
|
<span className="material-symbols-outlined">location_on</span>
|
|
</div>
|
|
<p>DUKANG HERITAGE SERVICE</p>
|
|
</div>
|
|
)}
|
|
</main>
|
|
|
|
<footer className="address-list-footer">
|
|
<Link
|
|
to={buildAddressEditUrl('new', checkoutCtx)}
|
|
className="address-list-add-btn"
|
|
onClick={(e) => e.stopPropagation()}
|
|
>
|
|
<span className="material-symbols-outlined">add</span>
|
|
<span>新增收货地址</span>
|
|
</Link>
|
|
</footer>
|
|
|
|
{pendingAddress && (
|
|
<div className="order-address-modal-overlay" onClick={() => setPendingAddress(null)}>
|
|
<div className="order-address-modal" onClick={(e) => e.stopPropagation()}>
|
|
<div className="order-address-modal-head">
|
|
<span className="material-symbols-outlined">location_on</span>
|
|
<h3>修改收货地址</h3>
|
|
</div>
|
|
<div className="order-address-modal-body">
|
|
<p>确认将订单收货地址修改为:</p>
|
|
<p className="order-address-modal-target">
|
|
{pendingAddress.receiverName} {maskPhone(pendingAddress.phone)}
|
|
<br />
|
|
{formatAddress(pendingAddress)}
|
|
</p>
|
|
</div>
|
|
<div className="order-address-modal-actions">
|
|
<button type="button" onClick={() => setPendingAddress(null)}>
|
|
取消
|
|
</button>
|
|
<button
|
|
type="button"
|
|
className="primary"
|
|
disabled={savingOrderAddress}
|
|
onClick={confirmOrderAddress}
|
|
>
|
|
{savingOrderAddress ? '保存中...' : '确认修改'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|