This commit is contained in:
2026-06-30 10:33:56 +08:00
commit 6e047dc0a5
607 changed files with 65966 additions and 0 deletions
+192
View File
@@ -0,0 +1,192 @@
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 { buildAddressEditUrl, buildAddressListUrl, buildOrderConfirmUrl, hasCheckoutContext, readCheckoutContext } from '../lib/navigation';
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() {
const [list, setList] = useState<Address[]>([]);
const [params] = useSearchParams();
const navigate = useNavigate();
const selectMode = params.get('select') === '1';
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]);
function selectAddress(id: string) {
if (!selectMode) 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', id);
navigate(`/order/confirm?${qs.toString()}`);
}
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 (hasCheckoutContext(checkoutCtx)) {
navigate(buildOrderConfirmUrl(checkoutCtx));
return;
}
navigate('/mine');
}
return (
<div className="address-list-page">
<SubPageHeader title="我的地址" 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 = 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(String(a.id))}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
selectAddress(String(a.id));
}
}}
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>
</div>
);
}