You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
195 lines
5.8 KiB
195 lines
5.8 KiB
"use client";
|
|
|
|
import { useEffect, useState } from "react";
|
|
import type { MothershipAddressCandidate } from "@/lib/frontend/types";
|
|
import { PrimaryButton } from "@/components/ui/primary-button";
|
|
|
|
export interface AddressDisambiguationModalProps {
|
|
open: boolean;
|
|
pickupCandidates: MothershipAddressCandidate[];
|
|
deliveryCandidates: MothershipAddressCandidate[];
|
|
onConfirm: (pickup: MothershipAddressCandidate, delivery: MothershipAddressCandidate) => void;
|
|
onCancel: () => void;
|
|
}
|
|
|
|
function isCandidateSelectable(item: MothershipAddressCandidate): boolean {
|
|
return item.selectable !== false;
|
|
}
|
|
|
|
function pickDefaultId(
|
|
candidates: MothershipAddressCandidate[],
|
|
): string | null {
|
|
if (candidates.length === 0) {
|
|
return null;
|
|
}
|
|
const selectable = candidates.filter(isCandidateSelectable);
|
|
if (selectable.length === 1) {
|
|
return selectable[0]!.option_id;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function findSelectableById(
|
|
candidates: MothershipAddressCandidate[],
|
|
id: string | null,
|
|
): MothershipAddressCandidate | null {
|
|
if (!id) {
|
|
return null;
|
|
}
|
|
const hit = candidates.find((c) => c.option_id === id);
|
|
if (!hit || !isCandidateSelectable(hit)) {
|
|
return null;
|
|
}
|
|
return hit;
|
|
}
|
|
|
|
function CandidateGroup({
|
|
title,
|
|
candidates,
|
|
selectedId,
|
|
onSelect,
|
|
}: {
|
|
title: string;
|
|
candidates: MothershipAddressCandidate[];
|
|
selectedId: string | null;
|
|
onSelect: (id: string) => void;
|
|
}) {
|
|
if (candidates.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
const multi = candidates.length > 1;
|
|
|
|
return (
|
|
<fieldset className="space-y-2">
|
|
<legend className="text-sm font-medium text-text-primary">{title}</legend>
|
|
<p className="text-xs text-text-secondary">
|
|
{multi
|
|
? "MotherShip 提供多个精确坐标,请选择与实际位置一致的一项"
|
|
: "请确认以下 MotherShip 解析结果是否与实际位置一致"}
|
|
</p>
|
|
<ul className="space-y-2">
|
|
{candidates.map((item) => {
|
|
const disabled = !isCandidateSelectable(item);
|
|
return (
|
|
<li key={item.option_id}>
|
|
<label
|
|
className={`flex items-start gap-3 rounded-md border border-border p-3 ${
|
|
disabled
|
|
? "cursor-not-allowed bg-surface-muted opacity-70"
|
|
: "cursor-pointer hover:bg-surface-muted"
|
|
}`}
|
|
>
|
|
<input
|
|
type="radio"
|
|
name={title}
|
|
className="mt-1"
|
|
disabled={disabled}
|
|
checked={!disabled && selectedId === item.option_id}
|
|
onChange={() => onSelect(item.option_id)}
|
|
/>
|
|
<span className="text-sm text-text-primary">
|
|
<span className="block">{item.display_label}</span>
|
|
{item.unavailable_reason ? (
|
|
<span className="mt-1 block text-xs text-error">
|
|
{item.unavailable_reason}
|
|
</span>
|
|
) : null}
|
|
</span>
|
|
</label>
|
|
</li>
|
|
);
|
|
})}
|
|
</ul>
|
|
</fieldset>
|
|
);
|
|
}
|
|
|
|
export function AddressDisambiguationModal({
|
|
open,
|
|
pickupCandidates,
|
|
deliveryCandidates,
|
|
onConfirm,
|
|
onCancel,
|
|
}: AddressDisambiguationModalProps) {
|
|
const [pickupId, setPickupId] = useState<string | null>(null);
|
|
const [deliveryId, setDeliveryId] = useState<string | null>(null);
|
|
|
|
useEffect(() => {
|
|
if (!open) {
|
|
return;
|
|
}
|
|
setPickupId(pickDefaultId(pickupCandidates));
|
|
setDeliveryId(pickDefaultId(deliveryCandidates));
|
|
}, [open, pickupCandidates, deliveryCandidates]);
|
|
|
|
if (!open) {
|
|
return null;
|
|
}
|
|
|
|
const pickup = findSelectableById(pickupCandidates, pickupId);
|
|
const delivery = findSelectableById(deliveryCandidates, deliveryId);
|
|
const canConfirm = pickup != null && delivery != null;
|
|
|
|
const handleConfirm = () => {
|
|
if (!pickup || !delivery) {
|
|
return;
|
|
}
|
|
onConfirm(pickup, delivery);
|
|
};
|
|
|
|
const hasMultiple =
|
|
pickupCandidates.length > 1 || deliveryCandidates.length > 1;
|
|
|
|
return (
|
|
<div
|
|
className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-4"
|
|
role="dialog"
|
|
aria-modal="true"
|
|
aria-labelledby="ms-address-title"
|
|
>
|
|
<div className="max-h-[90vh] w-full max-w-lg overflow-y-auto rounded-lg border border-border bg-surface p-6 shadow-card">
|
|
<h2
|
|
id="ms-address-title"
|
|
className="text-lg font-semibold text-text-primary"
|
|
>
|
|
确认 MotherShip 精确地址
|
|
</h2>
|
|
<p className="mt-2 text-sm text-text-secondary">
|
|
{hasMultiple
|
|
? "系统检测到 MotherShip 对您的地址有多个精确坐标。错误选择会导致报价不准,请逐项确认后再获取报价。"
|
|
: "请确认 MotherShip 解析的提货/派送地址是否准确,确认无误后再获取报价。"}
|
|
</p>
|
|
|
|
<div className="mt-6 space-y-6">
|
|
<CandidateGroup
|
|
title="提货地址"
|
|
candidates={pickupCandidates}
|
|
selectedId={pickupId}
|
|
onSelect={setPickupId}
|
|
/>
|
|
<CandidateGroup
|
|
title="派送地址"
|
|
candidates={deliveryCandidates}
|
|
selectedId={deliveryId}
|
|
onSelect={setDeliveryId}
|
|
/>
|
|
</div>
|
|
|
|
<div className="mt-6 flex flex-wrap gap-3">
|
|
<PrimaryButton disabled={!canConfirm} onClick={handleConfirm}>
|
|
确认并获取报价
|
|
</PrimaryButton>
|
|
<button
|
|
type="button"
|
|
className="rounded-md border border-border px-4 py-2 text-sm text-text-secondary hover:bg-surface-muted"
|
|
onClick={onCancel}
|
|
>
|
|
返回修改
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|