/** * MotherShip 登录后一级查价界面(Create a new shipment) * 对齐官网 dashboard「创建新货件」字段结构;中文文案供嵌入宿主展示 */ "use client"; import { useCallback, useId, useMemo, useState, type FormEvent } from "react"; import { Plus, X, CaretDown } from "@phosphor-icons/react"; import { MothershipAddressSuggestField } from "@/components/mothership/mothership-address-suggest-field"; import type { MothershipAddressCandidate } from "@/lib/frontend/types"; import { QuoteFieldTooltip, QuoteFloatHint, QuotePageTitle, QuoteRequiredMark, QuoteSectionTitle, QuoteStepProgress, quoteAddRowCls, quoteInputCls, quoteInputErrCls, } from "@/components/quote/quote-form-chrome"; import { MothershipWeekdayDatePicker } from "@/components/mothership/mothership-weekday-date-picker"; import { defaultMothershipReadyDateIso, isMothershipWeightEachAllowed, MOTHERSHIP_MAX_WEIGHT_EACH_LB, normalizeMothershipReadyDateIso, } from "@/lib/mothership/logged-in-constraints"; import { evaluateMothershipAccessorialCompat, mothershipAccessorialHasBlock, MS_PICKUP_BLOCKED_ACCESSORIALS, toggleMothershipAccessorial, type MsCompatIssue, } from "@/lib/mothership/option-compat"; export const MS_PICKUP_ACCESSORIALS = [ { id: "cfs", label: "CFS" }, { id: "liftgate", label: "尾板(Liftgate)" }, { id: "limitedAccess", label: "受限通道" }, { id: "inside", label: "LTL 室内服务" }, { id: "residential", label: "住宅" }, { id: "tradeshow", label: "展会" }, ] as const; export const MS_DELIVERY_ACCESSORIALS = [ { id: "fbaAppointment", label: "Amazon 预约" }, { id: "appointment", label: "预约" }, { id: "cfs", label: "CFS" }, { id: "liftgate", label: "尾板(Liftgate)" }, { id: "limitedAccess", label: "受限通道" }, { id: "inside", label: "LTL 室内服务" }, { id: "residential", label: "住宅" }, { id: "tradeshow", label: "展会" }, ] as const; /** 对齐官网 Cargo type 下拉(录制/截图 2026-07-15) */ export const MS_CARGO_TYPES = [ { id: "pallet", label: "托盘(Pallet)" }, { id: "box", label: "箱(Box)" }, { id: "crate", label: "木箱(Crate)" }, { id: "piece", label: "件(Piece)" }, { id: "bale", label: "捆包(Bale)" }, { id: "bucket", label: "桶(Bucket)" }, { id: "carton", label: "纸箱(Carton)" }, { id: "case", label: "箱件(Case)" }, { id: "coil", label: "卷材(Coil)" }, { id: "cylinder", label: "圆筒(Cylinder)" }, { id: "drum", label: "圆桶(Drum)" }, { id: "pail", label: "提桶(Pail)" }, { id: "reel", label: "线盘(Reel)" }, { id: "roll", label: "卷(Roll)" }, { id: "skid", label: "滑托(Skid)" }, { id: "tote", label: "集装袋(Tote)" }, { id: "tube", label: "管(Tube)" }, ] as const; /** 官网 Ready for pick-up 时刻:整点、12 小时制、有序无重复 */ export const MS_READY_TIMES: readonly string[] = (() => { const labels: string[] = []; for (let h = 0; h < 24; h += 1) { const hour12 = h % 12 === 0 ? 12 : h % 12; const suffix = h < 12 ? "AM" : "PM"; labels.push(`${hour12}:00 ${suffix}`); } return labels; })(); export const MS_DEFAULT_READY_TIME = "11:00 AM"; export type MsAccessorialId = | (typeof MS_PICKUP_ACCESSORIALS)[number]["id"] | (typeof MS_DELIVERY_ACCESSORIALS)[number]["id"]; export type MsCargoTypeId = (typeof MS_CARGO_TYPES)[number]["id"]; export type MsCargoLine = { key: string; cargoType: MsCargoTypeId; quantity: string; weightLb: string; lengthIn: string; widthIn: string; heightIn: string; }; export type MothershipLoggedInShipmentPayload = { pickupQuery: string; deliveryQuery: string; /** MotherShip 联想点选结果(继续闸门必填) */ pickupConfirmed: MothershipAddressCandidate; deliveryConfirmed: MothershipAddressCandidate; pickupAccessorials: string[]; deliveryAccessorials: string[]; readyDate: string; readyTime: string; timezone: string; cargo: Array<{ cargoType: MsCargoTypeId; quantity: number; weightLb: number; lengthIn: number; widthIn: number; heightIn: number; }>; }; export interface MothershipLoggedInShipmentFormProps { formId?: string; disabled?: boolean; customerId: string; apiBaseUrl?: string; serviceToken?: string; onValidSubmit?: (payload: MothershipLoggedInShipmentPayload) => void; } function newCargoLine(): MsCargoLine { return { key: `c-${Math.random().toString(36).slice(2, 10)}`, cargoType: "pallet", quantity: "", weightLb: "", lengthIn: "", widthIn: "", heightIn: "", }; } function RequiredMark() { return ; } function AccessorialMultiSelect({ label, options, selected, disabled, tip, side, onChange, }: { label: string; options: readonly { id: string; label: string }[]; selected: string[]; disabled?: boolean; tip?: string; side: "pickup" | "delivery"; onChange: (next: string[], meta?: { message?: string; issues: MsCompatIssue[] }) => void; }) { const [open, setOpen] = useState(false); const [hint, setHint] = useState(null); const summary = useMemo(() => { if (selected.length === 0) return "选择"; if (selected.length === 1) { return options.find((o) => o.id === selected[0])?.label ?? "已选择"; } const first = options.find((o) => o.id === selected[0])?.label ?? selected[0]; return `${first} 等 ${selected.length} 项`; }, [options, selected]); return ( {label} {tip ? : null} setOpen((v) => !v)} className="flex h-10 w-full items-center justify-between rounded-md border border-border bg-surface px-3 text-left text-sm transition-colors hover:border-[#1890FF]/40 focus:border-[#1890FF] focus:outline-none focus:ring-2 focus:ring-[#1890FF]/20 disabled:cursor-not-allowed disabled:opacity-60" > {summary} {open && !disabled && ( {options.map((opt) => { const checked = selected.includes(opt.id); const pickupBlocked = side === "pickup" && Boolean(MS_PICKUP_BLOCKED_ACCESSORIALS[opt.id]); const blockedReason = pickupBlocked ? MS_PICKUP_BLOCKED_ACCESSORIALS[opt.id] : undefined; return ( { const r = toggleMothershipAccessorial({ side, selected, id: opt.id, }); if (!r.applied) { setHint(r.message ?? null); return; } setHint(r.message ?? null); onChange(r.next, { message: r.message, issues: r.issues }); }} className="mt-0.5 h-4 w-4 rounded border-border text-[#1890FF] focus:ring-[#1890FF]/30 disabled:cursor-not-allowed" /> {opt.label} {blockedReason ? ( {blockedReason} ) : null} ); })} )} {hint ? ( {hint} ) : null} {selected.length > 0 && ( {selected.map((id) => { const opt = options.find((o) => o.id === id); return ( {opt?.label ?? id} ); })} )} ); } /** MotherShip 登录后一级:创建新货件 */ export function MothershipLoggedInShipmentForm({ formId = "ms-logged-in-shipment-form", disabled, customerId, apiBaseUrl = "", serviceToken, onValidSubmit, }: MothershipLoggedInShipmentFormProps) { const baseId = useId(); const [pickupQuery, setPickupQuery] = useState(""); const [deliveryQuery, setDeliveryQuery] = useState(""); const [pickupConfirmed, setPickupConfirmed] = useState(null); const [deliveryConfirmed, setDeliveryConfirmed] = useState(null); const [pickupAccessorials, setPickupAccessorials] = useState([]); const [deliveryAccessorials, setDeliveryAccessorials] = useState( [], ); const [readyDate, setReadyDate] = useState(defaultMothershipReadyDateIso); const [readyTime, setReadyTime] = useState(MS_DEFAULT_READY_TIME); const [timezone] = useState("GMT+8"); const [cargoLines, setCargoLines] = useState([newCargoLine()]); const [localError, setLocalError] = useState(null); const [invalidFields, setInvalidFields] = useState>(new Set()); const accessorialHints = useMemo(() => { return [ ...evaluateMothershipAccessorialCompat({ side: "pickup", selected: pickupAccessorials, }), ...evaluateMothershipAccessorialCompat({ side: "delivery", selected: deliveryAccessorials, }), ]; }, [pickupAccessorials, deliveryAccessorials]); const updateCargo = useCallback( (key: string, patch: Partial) => { setCargoLines((rows) => rows.map((r) => (r.key === key ? { ...r, ...patch } : r)), ); }, [], ); const handleSubmit = (e: FormEvent) => { e.preventDefault(); setLocalError(null); const nextInvalid = new Set(); if (!pickupConfirmed) { nextInvalid.add("pickup"); setInvalidFields(nextInvalid); setLocalError("请填写提货地址"); return; } if (!deliveryConfirmed) { nextInvalid.add("delivery"); setInvalidFields(nextInvalid); setLocalError("请填写送货地址"); return; } const pickupIssues = evaluateMothershipAccessorialCompat({ side: "pickup", selected: pickupAccessorials, }); const deliveryIssues = evaluateMothershipAccessorialCompat({ side: "delivery", selected: deliveryAccessorials, }); const blockMsg = [...pickupIssues, ...deliveryIssues].find( (i) => i.severity === "block", )?.message; if (blockMsg) { setLocalError(blockMsg); return; } const cargo: MothershipLoggedInShipmentPayload["cargo"] = []; for (const line of cargoLines) { const quantity = Number(line.quantity); const weightLb = Number(line.weightLb); const lengthIn = Number(line.lengthIn); const widthIn = Number(line.widthIn); const heightIn = Number(line.heightIn); if (!Number.isFinite(quantity) || quantity < 1) { nextInvalid.add(`${line.key}-quantity`); } if (!Number.isFinite(weightLb) || weightLb <= 0) { nextInvalid.add(`${line.key}-weight`); } else if (!isMothershipWeightEachAllowed(weightLb)) { nextInvalid.add(`${line.key}-weight`); setInvalidFields(nextInvalid); setLocalError( `单件重量不能超过 ${MOTHERSHIP_MAX_WEIGHT_EACH_LB} 磅`, ); return; } if (!Number.isFinite(lengthIn) || lengthIn <= 0) { nextInvalid.add(`${line.key}-length`); } if (!Number.isFinite(widthIn) || widthIn <= 0) { nextInvalid.add(`${line.key}-width`); } if (!Number.isFinite(heightIn) || heightIn <= 0) { nextInvalid.add(`${line.key}-height`); } if ( nextInvalid.has(`${line.key}-quantity`) || nextInvalid.has(`${line.key}-weight`) || nextInvalid.has(`${line.key}-length`) || nextInvalid.has(`${line.key}-width`) || nextInvalid.has(`${line.key}-height`) ) { setInvalidFields(nextInvalid); setLocalError("请完整填写每行货物的数量、重量与尺寸"); return; } cargo.push({ cargoType: line.cargoType, quantity, weightLb, lengthIn, widthIn, heightIn, }); } setInvalidFields(new Set()); const normalizedReadyDate = normalizeMothershipReadyDateIso(readyDate) ?? readyDate; onValidSubmit?.({ pickupQuery: pickupConfirmed.display_label || pickupConfirmed.formatted_address || pickupQuery.trim(), deliveryQuery: deliveryConfirmed.display_label || deliveryConfirmed.formatted_address || deliveryQuery.trim(), pickupConfirmed, deliveryConfirmed, pickupAccessorials, deliveryAccessorials, readyDate: normalizedReadyDate, readyTime, timezone, cargo, }); }; const fieldCls = (key: string) => invalidFields.has(key) ? quoteInputErrCls : quoteInputCls; return ( 创建新货件 {accessorialHints.length > 0 ? ( 附加服务提示 {accessorialHints.map((i) => ( {i.message} ))} ) : null} {/* 地址 + 时间 */} 提货与送货 { setPickupQuery(q); setInvalidFields((prev) => { const next = new Set(prev); next.delete("pickup"); return next; }); }} onConfirm={(c) => { setPickupConfirmed(c); if (c) { setInvalidFields((prev) => { const next = new Set(prev); next.delete("pickup"); return next; }); setLocalError(null); } }} /> { setPickupAccessorials(next); if (meta?.message) setLocalError(meta.message); else if ( meta?.issues && mothershipAccessorialHasBlock(meta.issues) ) { setLocalError( meta.issues.find((i) => i.severity === "block")!.message, ); } }} /> → { setDeliveryQuery(q); setInvalidFields((prev) => { const next = new Set(prev); next.delete("delivery"); return next; }); }} onConfirm={(c) => { setDeliveryConfirmed(c); if (c) { setInvalidFields((prev) => { const next = new Set(prev); next.delete("delivery"); return next; }); setLocalError(null); } }} /> { setDeliveryAccessorials(next); if (meta?.message) setLocalError(meta.message); }} /> 可提货时间 之后 可提货时刻 setReadyTime(e.target.value)} className={`${quoteInputCls} min-w-[7.5rem]`} > {MS_READY_TIMES.map((t) => ( {t} ))} {timezone} {/* 货物 */} 货物 setCargoLines((rows) => [...rows, newCargoLine()])} className={quoteAddRowCls} > 添加货物行 {cargoLines.map((line, idx) => ( 货物类型 updateCargo(line.key, { cargoType: e.target.value as MsCargoTypeId, }) } className={quoteInputCls} > {MS_CARGO_TYPES.map((t) => ( {t.label} ))} 数量 updateCargo(line.key, { quantity: e.target.value }) } className={fieldCls(`${line.key}-quantity`)} /> 单件重量 updateCargo(line.key, { weightLb: e.target.value }) } className={fieldCls(`${line.key}-weight`)} /> 单件长 updateCargo(line.key, { lengthIn: e.target.value }) } className={fieldCls(`${line.key}-length`)} /> 单件宽 updateCargo(line.key, { widthIn: e.target.value }) } className={fieldCls(`${line.key}-width`)} /> 单件高 updateCargo(line.key, { heightIn: e.target.value }) } className={fieldCls(`${line.key}-height`)} /> setCargoLines((rows) => rows.filter((r) => r.key !== line.key), ) } className="inline-flex h-10 w-10 items-center justify-center rounded-md border border-border text-text-secondary transition-colors hover:border-[#EF4444] hover:text-[#EF4444] disabled:cursor-not-allowed disabled:opacity-40" aria-label="删除该行货物" > ))} ); }
{hint}
附加服务提示