/** * Flock Freight 登录后 Quick 查价界面(Let's build a quote) * 对齐录制 flock-logged-in-20260715-143321.js + 官网截图;中文文案供嵌入宿主展示 */ "use client"; import { useId, useMemo, useState, useEffect, type FormEvent, type ReactNode } from "react"; import { Plus, Trash, CaretDown } from "@phosphor-icons/react"; import { defaultFlockPickupDateIso, flockPickupDateValidationMessageFromIso, minFlockPickupDateIso, } from "@/lib/flock/pickup-date"; import { MothershipWeekdayDatePicker } from "@/components/mothership/mothership-weekday-date-picker"; import { FLOCK_LIMITS, FLOCK_VALIDATION_MESSAGES, } from "@/lib/constants/flock-limits"; import { evaluateFlockOptionCompat, flockOptionHasBlock, isFlockReeferOnlyWithoutTemp, isFlockSprinterOnly, isFlockTempWithoutReefer, } from "@/lib/flock/option-compat"; import { FlockSavedFreightSearch, FlockSaveFreightButton, } from "@/components/flock/flock-saved-freight-controls"; import type { FlockSavedFreightPreset } from "@/lib/flock/saved-freight-presets"; import { QuoteFieldTooltip, QuoteFloatHint, QuoteNoticeCollapse, QuotePageTitle, QuoteRequiredMark, QuoteSectionTitle, QuoteStepProgress, QuoteTipDefs, quoteAddRowCls, quoteCtaCls, quoteInputCls, quoteInputErrCls, } from "@/components/quote/quote-form-chrome"; export const FLOCK_LOCATION_TYPES = [ { id: "business_with_dock", label: "商业地址(有月台)" }, { id: "business_without_dock", label: "商业地址(无月台)" }, { id: "residential", label: "住宅地址" }, { id: "trade_show", label: "展会 / 展览" }, { id: "limited_construction", label: "施工工地" }, { id: "limited_farm", label: "农场 / 种养殖地" }, { id: "limited_airport", label: "受限通道 · 机场" }, { id: "limited_port", label: "受限通道 · 港口码头" }, { id: "limited_military", label: "受限通道 · 军事基地" }, { id: "limited_worship", label: "受限通道 · 宗教场所" }, { id: "limited_school", label: "受限通道 · 学校" }, { id: "limited_other", label: "受限通道 · 其他" }, ] as const; /** 各地点类型可见的附加选项(对齐 Flock 官网:类别不同选项不同) */ export type FlockLocationAccessorialFlags = { liftgate: boolean; inside: boolean; palletJack: boolean; }; export const FLOCK_LOCATION_ACCESSORIALS: Record< (typeof FLOCK_LOCATION_TYPES)[number]["id"], FlockLocationAccessorialFlags > = { // 有卸货台:无附加勾选项(官网无 Liftgate / Inside / Pallet Jack) business_with_dock: { liftgate: false, inside: false, palletJack: false }, // 无卸货台:三项全开 business_without_dock: { liftgate: true, inside: true, palletJack: true }, // 住宅:仅尾板 + 室内 residential: { liftgate: true, inside: true, palletJack: false }, // 展会:仅托盘车 trade_show: { liftgate: false, inside: false, palletJack: true }, // 施工现场:尾板 + 托盘车(无室内) limited_construction: { liftgate: true, inside: false, palletJack: true }, // 农场:三项 limited_farm: { liftgate: true, inside: true, palletJack: true }, limited_airport: { liftgate: true, inside: true, palletJack: true }, limited_port: { liftgate: true, inside: true, palletJack: true }, limited_military: { liftgate: true, inside: true, palletJack: true }, // 宗教场所:仅尾板 + 室内(截图 Limited Access – Church) limited_worship: { liftgate: true, inside: true, palletJack: false }, limited_school: { liftgate: true, inside: true, palletJack: true }, limited_other: { liftgate: true, inside: true, palletJack: true }, }; export function getFlockLocationAccessorials( typeId: (typeof FLOCK_LOCATION_TYPES)[number]["id"], ): FlockLocationAccessorialFlags { return FLOCK_LOCATION_ACCESSORIALS[typeId]; } export const FLOCK_PACKAGING_TYPES = [ { id: "pallets_48x40", label: "标准托盘 48×40", length: 48, width: 40 }, { id: "pallets_48x48", label: "标准托盘 48×48", length: 48, width: 48 }, { id: "pallets_60x48", label: "标准托盘 60×48", length: 60, width: 48 }, { id: "pallets_custom", label: "自定义托盘尺寸", length: null, width: null }, { id: "bags", label: "袋装", length: null, width: null }, { id: "bales", label: "捆包", length: null, width: null }, { id: "boxes", label: "纸箱 / 纸盒", length: null, width: null }, { id: "bundles", label: "捆扎件", length: null, width: null }, { id: "coils", label: "卷料", length: null, width: null }, { id: "crates", label: "木箱 / 板条箱", length: null, width: null }, { id: "cylinders", label: "钢瓶 / 气瓶", length: null, width: null }, { id: "drums", label: "铁桶 / 圆桶", length: null, width: null }, { id: "pails", label: "小桶 / 提桶", length: null, width: null }, { id: "reels", label: "线盘 / 卷盘", length: null, width: null }, { id: "rolls", label: "卷筒", length: null, width: null }, { id: "slipsheets", label: "滑托板", length: null, width: null }, { id: "tubes_pipes", label: "管材", length: null, width: null }, { id: "units", label: "单件 / 整机", length: null, width: null }, ] as const; export const FLOCK_FREIGHT_CLASSES = [ "density", "50", "55", "60", "65", "70", "77.5", "85", "92.5", "100", "110", "125", "150", "175", "200", "250", "300", "400", "500", ] as const; export const FLOCK_ADDITIONAL_SERVICES = [ { id: "blind_shipment", label: "盲运(隐藏发/收货方)" }, { id: "food_grade", label: "食品级车厢" }, { id: "load_to_ride", label: "全程直达(不换装)" }, { id: "temperature_control", label: "温控 / 冷链" }, { id: "unloading", label: "到站卸货服务" }, ] as const; export const FLOCK_VEHICLE_TYPES = [ { id: "box_truck", label: "厢式货车" }, { id: "dry_van", label: "干货挂车" }, { id: "refrigerated", label: "冷藏车" }, { id: "sprinter", label: "小型厢车" }, ] as const; export const FLOCK_DEFAULT_VEHICLES = ["box_truck", "dry_van", "refrigerated"] as const; /** 官网提示文案(货运行业惯用译,对齐全票提货/派送语境) */ export const FLOCK_LOCATION_TIP_INTRO = "请准确选择地点类型,便于承运商安排提货与派送车辆,减少压车与拒收。"; export const FLOCK_LOCATION_TIP_ITEMS = [ { title: "商业地址(有月台):", body: "具备标准高度装卸月台,车辆可直接对接上下货的商业场所。", }, { title: "商业地址(无月台):", body: "无标准装卸月台,通常需尾板或地面装卸的商业场所。", }, { title: "住宅地址:", body: "按当地区划认定的住宅地点,含住家办公、宅基地建筑等。", }, { title: "展会 / 展览:", body: "进出展馆、展台及展会临时仓的货件。", }, { title: "施工工地:", body: "正在施工、进出可能受限的工地现场。", }, { title: "农场 / 种养殖地:", body: "农业种植、养殖或农产品生产用地。", }, { title: "受限通道地点:", body: "进出不便,或需额外通行证/安检手续的地点(机场、港口、军营、校园等)。", }, ] as const; export const FLOCK_FREIGHT_CLASS_TIP = "填写件数、外尺寸(长×宽×高)与重量后,可据此估算或选择 NMFC 运价等级。"; export const FLOCK_NMFC_NOTICE = "仅按密度推算的等级可能不准确。正式分等还要考虑装卸难度、积载性与承运责任。托运人须选用正确的 NMFC 代码与等级,否则可能被重新分等并产生改费、延误与追加费用。"; export const FLOCK_STACKABLE_TIP = "货件可堆码或可调向摆放时,有助于提高装载率,运费最多约可省 20%。"; export const FLOCK_ADDITIONAL_SERVICE_TIP_ITEMS = [ { title: "盲运:", body: "提单及签收单据对发货方和/或收货方信息进行隐匿,适用于中转贸易等场景。", }, { title: "食品级车厢:", body: "要求车厢洁净、无异味、无破损渗透,适用于食品及敏感货物。", }, { title: "全程直达(不换装):", body: "要求货件途中不经其他站点翻装换车,降低破损与延误风险。", }, { title: "温控 / 冷链:", body: "车厢需按指定温度区间控温运输。", }, { title: "到站卸货服务:", body: "含到站分拣、装卸工协助等;不等于白手套式送货上门。", }, ] as const; export const FLOCK_VEHICLE_TIP_ITEMS = [ { title: "干货挂车:", body: "提货或派送场地可停靠约 53 英尺干货厢式挂车时勾选。", }, { title: "冷藏车:", body: "货件可装入各类尺寸冷藏车(如 48′、53′)时勾选;若需设定温度,请同时勾选「温控 / 冷链」。", }, { title: "厢式货车:", body: "场地可停靠各尺寸厢货(如 18′–28′ 城市配送车)时勾选。", }, { title: "小型厢车:", body: "场地可停靠厢式面包车/小型厢货,且无需叉车或坡道装卸时勾选。", }, ] as const; export const FLOCK_INSURANCE_YES_TIP = "若需在承运人责任限额之外另行加保,请选「是」。"; export const FLOCK_INSURANCE_NO_TIP = "货件已有保险,或不需超出承运人责任限额的额外保额时,请选「否」。"; export const FLOCK_INSURANCE_WAIVE_HINT = "您已选择不投保额外货运险。"; export const FLOCK_INSURANCE_FOOTER = "如需了解货运保险细则,可联系 support@flockfreight.com。"; /** 整票总重未超过阈值时,官网调度区部分选项为灰色不可选 */ export function isFlockSchedulingOptionLocked( totalWeightLb: number, weightLocked: boolean, ): boolean { if (!weightLocked) return false; return !Number.isFinite(totalWeightLb) || totalWeightLb <= FLOCK_LIMITS.schedulingWeightMinLb; } export function flockSchedulingWeightLockedHint(): string { return `整票总重须大于 ${FLOCK_LIMITS.schedulingWeightMinLb} lb 才可选用`; } /** 官网时段下拉(12h,半小时步进) */ export const FLOCK_SCHEDULING_TIME_OPTIONS: string[] = (() => { const opts: string[] = []; for (let h = 0; h < 24; h += 1) { for (const m of [0, 30] as const) { const hour12 = h % 12 === 0 ? 12 : h % 12; const ampm = h < 12 ? "am" : "pm"; opts.push(`${hour12}:${m === 0 ? "00" : "30"} ${ampm}`); } } return opts; })(); /** Quick 调度:weightLocked=true 表示总重 ≤5000 时灰色禁用 */ export const FLOCK_PICKUP_SERVICES = [ { id: "standard_fcfs", label: "标准提货(FCFS)", hint: "在地点营业时间内提货", detail: "", weightLocked: false, }, { id: "during_window", label: "指定提货时段", hint: "", detail: "提货地点有预设时段(如 2PM–4PM)可供提货。", weightLocked: true, }, { id: "have_appointment", label: "已有提货预约", hint: "", detail: "", weightLocked: true, }, { id: "need_appointment", label: "需要提货预约", hint: "", detail: "", weightLocked: true, }, ] as const; /** 提货前致电:总重 ≤5000 时灰色 */ export const FLOCK_PICKUP_CALL_BEFORE_WEIGHT_LOCKED = true; export const FLOCK_DELIVERY_SERVICES = [ { id: "standard_fcfs", label: "标准派送(FCFS)", hint: "在地点营业时间内派送", detail: "", weightLocked: false, }, { id: "during_window", label: "指定派送时段", hint: "", detail: "派送地点有预设时段(如 2PM–4PM)可供派送。", weightLocked: true, }, { id: "have_appointment", label: "已有派送预约", hint: "", detail: "", weightLocked: true, }, { id: "need_appointment", label: "需要派送预约", hint: "", detail: "", /** 官网小批量下仍可选 */ weightLocked: false, }, { id: "must_arrive_by", label: "须在指定日期前到达", hint: "", detail: "", weightLocked: true, }, ] as const; export const FLOCK_CALL_FOR_DELIVERY_APPOINTMENT_HINT = "Flock Freight 将与派送地点及承运商协调预约。"; export type FlockLocationTypeId = (typeof FLOCK_LOCATION_TYPES)[number]["id"]; export type FlockPackagingTypeId = (typeof FLOCK_PACKAGING_TYPES)[number]["id"]; export type FlockFreightClass = (typeof FLOCK_FREIGHT_CLASSES)[number]; export type FlockAdditionalServiceId = (typeof FLOCK_ADDITIONAL_SERVICES)[number]["id"]; export type FlockVehicleTypeId = (typeof FLOCK_VEHICLE_TYPES)[number]["id"]; export type FlockLoggedInItem = { key: string; quantity: string; packagingType: FlockPackagingTypeId; lengthIn: string; widthIn: string; heightIn: string; totalWeightLb: string; freightClass: FlockFreightClass; description: string; stackable: boolean; turnable: boolean; }; export type FlockLoggedInQuotePayload = { mode: "quick"; pickupDate: string; pickupZip: string; pickupType: FlockLocationTypeId; pickupLiftgate: boolean; pickupInside: boolean; pickupPalletJack: boolean; deliveryZip: string; deliveryType: FlockLocationTypeId; deliveryLiftgate: boolean; deliveryInside: boolean; deliveryPalletJack: boolean; items: Array<{ quantity: number; packagingType: FlockPackagingTypeId; lengthIn: number; widthIn: number; heightIn: number; totalWeightLb: number; freightClass: FlockFreightClass; description: string; stackable: boolean; turnable: boolean; }>; additionalServices: FlockAdditionalServiceId[]; vehicleTypes: FlockVehicleTypeId[]; pickupService: string; deliveryService: string; /** 指定提货时段 */ pickupWindowStartTime?: string; pickupWindowEndTime?: string; /** 已有提货预约 */ pickupAppointmentTime?: string; /** 指定派送时段 */ deliveryWindowStartTime?: string; deliveryWindowEndTime?: string; deliveryWindowDate?: string; /** 已有派送预约 */ deliveryAppointmentTime?: string; deliveryAppointmentDate?: string; /** 须在指定日期前到达 */ deliveryMustArriveByDate?: string; /** Must arrive by 下的「预约派送致电」 */ callForDeliveryAppointment?: boolean; callBeforePickup: boolean; callBeforeDelivery: boolean; additionalInsurance: boolean; }; export interface FlockLoggedInQuoteFormProps { formId?: string; disabled?: boolean; submitLocked?: boolean; /** 按客户隔离常用货件(localStorage) */ customerId?: string; onValidSubmit?: (payload: FlockLoggedInQuotePayload) => void; } function RequiredMark() { return ; } function newItem(): FlockLoggedInItem { return { key: `fi-${Math.random().toString(36).slice(2, 10)}`, quantity: "", packagingType: "pallets_48x40", lengthIn: "48", widthIn: "40", heightIn: "", totalWeightLb: "", freightClass: "density", description: "", stackable: false, turnable: false, }; } function applyPackagingDims( packagingType: FlockPackagingTypeId, item: FlockLoggedInItem, ): FlockLoggedInItem { const pack = FLOCK_PACKAGING_TYPES.find((p) => p.id === packagingType); if (!pack) return { ...item, packagingType }; return { ...item, packagingType, lengthIn: pack.length != null ? String(pack.length) : item.lengthIn, widthIn: pack.width != null ? String(pack.width) : item.widthIn, }; } function applySavedFreightPreset( item: FlockLoggedInItem, preset: FlockSavedFreightPreset, ): FlockLoggedInItem { const packOk = FLOCK_PACKAGING_TYPES.some((p) => p.id === preset.packagingType); const classOk = (FLOCK_FREIGHT_CLASSES as readonly string[]).includes( preset.freightClass, ); return { ...item, quantity: preset.quantity || item.quantity, packagingType: packOk ? (preset.packagingType as FlockPackagingTypeId) : item.packagingType, lengthIn: preset.lengthIn || item.lengthIn, widthIn: preset.widthIn || item.widthIn, heightIn: preset.heightIn || item.heightIn, totalWeightLb: preset.totalWeightLb || item.totalWeightLb, freightClass: classOk ? (preset.freightClass as FlockFreightClass) : item.freightClass, description: preset.description || preset.name, stackable: Boolean(preset.stackable), turnable: Boolean(preset.turnable), }; } function OutlinedField({ label, required, error, tip, children, }: { label: string; required?: boolean; error?: string; tip?: ReactNode; children: ReactNode; }) { return ( ); } const inputCls = quoteInputCls; const inputErrCls = quoteInputErrCls; function LocationAccessorialChecks({ side, typeId, disabled, liftgate, inside, palletJack, onLiftgate, onInside, onPalletJack, }: { side: "pickup" | "delivery"; typeId: FlockLocationTypeId; disabled?: boolean; liftgate: boolean; inside: boolean; palletJack: boolean; onLiftgate: (v: boolean) => void; onInside: (v: boolean) => void; onPalletJack: (v: boolean) => void; }) { const flags = getFlockLocationAccessorials(typeId); if (!flags.liftgate && !flags.inside && !flags.palletJack) return null; const liftgateLabel = side === "pickup" ? "提货需要尾板(Liftgate)" : "派送需要尾板(Liftgate)"; const insideLabel = side === "pickup" ? "室内提货(Inside Pickup)" : "室内派送(Inside Delivery)"; return (
{flags.liftgate ? ( ) : null} {flags.inside ? ( ) : null} {flags.palletJack ? ( ) : null}
); } function ChipToggle({ selected, disabled, label, onClick, }: { selected: boolean; disabled?: boolean; label: string; onClick: () => void; }) { return ( ); } /** 登录态货物行尺寸/重量硬限(同源 FLOCK_LIMITS) */ export function validateFlockLoggedInCargoLine(input: { quantity: number; lengthIn: number; widthIn: number; heightIn: number; totalWeightLb: number; description: string; }): Record { const err: Record = {}; const { min, lengthMax, widthMax, heightMax } = FLOCK_LIMITS.dimIn; if (!Number.isFinite(input.quantity) || input.quantity <= 0) { err.qty = "件数必填"; } if (!Number.isFinite(input.lengthIn) || input.lengthIn < min) { err.len = "长度必填"; } else if (input.lengthIn > lengthMax) { err.len = FLOCK_VALIDATION_MESSAGES.length; } if (!Number.isFinite(input.widthIn) || input.widthIn < min) { err.wid = "宽度必填"; } else if (input.widthIn > widthMax) { err.wid = FLOCK_VALIDATION_MESSAGES.width; } if (!Number.isFinite(input.heightIn) || input.heightIn < min) { err.hei = "高度必填"; } else if (input.heightIn > heightMax) { err.hei = FLOCK_VALIDATION_MESSAGES.height; } if (!Number.isFinite(input.totalWeightLb) || input.totalWeightLb <= 0) { err.wt = "重量必填"; } else if (input.totalWeightLb > FLOCK_LIMITS.totalWeightLbMax) { err.wt = FLOCK_VALIDATION_MESSAGES.totalWeight; } if (!input.description.trim()) { err.desc = "货物描述必填"; } return err; } /** 将登录态 payload 映射为 Flock API flock_input(含登录态 RPA 扩展字段) */ export function mapFlockLoggedInToApiInput(payload: FlockLoggedInQuotePayload): { pickup_date: string; pickup_zip: string; delivery_zip: string; pallet_count: number; total_weight: { value: number; unit: "lb" }; dimensions: { length: number; width: number; height: number; unit: "in" }; form_mode: "logged_in_quick"; pickup_location_type: string; delivery_location_type: string; packaging_type: string; freight_class: string; description: string; stackable: boolean; turnable: boolean; additional_services: string[]; vehicle_types: string[]; call_before_pickup: boolean; call_before_delivery: boolean; pickup_service: string; delivery_service: string; pickup_window_start_time?: string; pickup_window_end_time?: string; pickup_appointment_time?: string; delivery_window_start_time?: string; delivery_window_end_time?: string; delivery_window_date?: string; delivery_appointment_time?: string; delivery_appointment_date?: string; delivery_must_arrive_by_date?: string; call_for_delivery_appointment?: boolean; pickup_liftgate: boolean; pickup_inside: boolean; pickup_pallet_jack: boolean; delivery_liftgate: boolean; delivery_inside: boolean; delivery_pallet_jack: boolean; } { const [y, m, d] = payload.pickupDate.split("-"); const pickup_date = y && m && d ? `${m}/${d}/${y}` : payload.pickupDate; const first = payload.items[0]; const pallet_count = payload.items.reduce((s, it) => s + it.quantity, 0); const total_weight = payload.items.reduce((s, it) => s + it.totalWeightLb, 0); const opt = (v?: string) => { const t = (v ?? "").trim(); return t || undefined; }; return { pickup_date, pickup_zip: payload.pickupZip, delivery_zip: payload.deliveryZip, pallet_count: Math.max(1, pallet_count), total_weight: { value: total_weight || first?.totalWeightLb || 1, unit: "lb" }, dimensions: { length: first?.lengthIn ?? 48, width: first?.widthIn ?? 40, height: first?.heightIn ?? 48, unit: "in", }, form_mode: "logged_in_quick", pickup_location_type: payload.pickupType, delivery_location_type: payload.deliveryType, packaging_type: first?.packagingType ?? "pallets_48x40", freight_class: first?.freightClass ?? "density", description: first?.description?.trim() || "General freight", stackable: Boolean(first?.stackable), turnable: Boolean(first?.turnable), additional_services: [...payload.additionalServices], vehicle_types: [...payload.vehicleTypes], call_before_pickup: FLOCK_PICKUP_CALL_BEFORE_WEIGHT_LOCKED && isFlockSchedulingOptionLocked( payload.items.reduce((s, it) => s + it.totalWeightLb, 0), true, ) ? false : Boolean(payload.callBeforePickup), call_before_delivery: Boolean(payload.callBeforeDelivery), pickup_service: payload.pickupService, delivery_service: payload.deliveryService, pickup_window_start_time: opt(payload.pickupWindowStartTime), pickup_window_end_time: opt(payload.pickupWindowEndTime), pickup_appointment_time: opt(payload.pickupAppointmentTime), delivery_window_start_time: opt(payload.deliveryWindowStartTime), delivery_window_end_time: opt(payload.deliveryWindowEndTime), delivery_window_date: opt(payload.deliveryWindowDate), delivery_appointment_time: opt(payload.deliveryAppointmentTime), delivery_appointment_date: opt(payload.deliveryAppointmentDate), delivery_must_arrive_by_date: opt(payload.deliveryMustArriveByDate), call_for_delivery_appointment: payload.deliveryService === "must_arrive_by" ? Boolean(payload.callForDeliveryAppointment) : undefined, pickup_liftgate: Boolean(payload.pickupLiftgate), pickup_inside: Boolean(payload.pickupInside), pickup_pallet_jack: Boolean(payload.pickupPalletJack), delivery_liftgate: Boolean(payload.deliveryLiftgate), delivery_inside: Boolean(payload.deliveryInside), delivery_pallet_jack: Boolean(payload.deliveryPalletJack), }; } export function FlockLoggedInQuoteForm({ formId = "flock-logged-in-quote-form", disabled, submitLocked, customerId = "anon", onValidSubmit, }: FlockLoggedInQuoteFormProps) { const baseId = useId(); const [pickupDate, setPickupDate] = useState(defaultFlockPickupDateIso); const [pickupZip, setPickupZip] = useState(""); const [pickupType, setPickupType] = useState("business_with_dock"); const [pickupLiftgate, setPickupLiftgate] = useState(false); const [pickupInside, setPickupInside] = useState(false); const [pickupPalletJack, setPickupPalletJack] = useState(false); const [deliveryZip, setDeliveryZip] = useState(""); const [deliveryType, setDeliveryType] = useState("business_with_dock"); const [deliveryLiftgate, setDeliveryLiftgate] = useState(false); const [deliveryInside, setDeliveryInside] = useState(false); const [deliveryPalletJack, setDeliveryPalletJack] = useState(false); const [items, setItems] = useState(() => [newItem()]); const [savedFreightRevision, setSavedFreightRevision] = useState(0); const [additionalServices, setAdditionalServices] = useState< FlockAdditionalServiceId[] >(["unloading"]); const [vehicleTypes, setVehicleTypes] = useState([ ...FLOCK_DEFAULT_VEHICLES, ]); const [pickupService, setPickupService] = useState("standard_fcfs"); const [deliveryService, setDeliveryService] = useState("standard_fcfs"); const [pickupWindowStartTime, setPickupWindowStartTime] = useState("8:00 am"); const [pickupWindowEndTime, setPickupWindowEndTime] = useState("5:00 pm"); const [pickupAppointmentTime, setPickupAppointmentTime] = useState("8:00 am"); const [deliveryWindowStartTime, setDeliveryWindowStartTime] = useState("8:00 am"); const [deliveryWindowEndTime, setDeliveryWindowEndTime] = useState("5:00 pm"); const [deliveryWindowDate, setDeliveryWindowDate] = useState(""); const [deliveryAppointmentTime, setDeliveryAppointmentTime] = useState("8:00 am"); const [deliveryAppointmentDate, setDeliveryAppointmentDate] = useState(""); const [deliveryMustArriveByDate, setDeliveryMustArriveByDate] = useState(""); const [callForDeliveryAppointment, setCallForDeliveryAppointment] = useState(false); const [callBeforePickup, setCallBeforePickup] = useState(false); const [callBeforeDelivery, setCallBeforeDelivery] = useState(false); const [additionalInsurance, setAdditionalInsurance] = useState(false); const [showAdditional, setShowAdditional] = useState(true); const [showScheduling, setShowScheduling] = useState(true); const [showInsurance, setShowInsurance] = useState(true); const [errors, setErrors] = useState>({}); const [localError, setLocalError] = useState(null); const servicesSelected = additionalServices.length; const vehiclesSelected = vehicleTypes.length; const shipmentWeightLb = useMemo( () => items.reduce((sum, it) => { const w = Number(it.totalWeightLb); return sum + (Number.isFinite(w) && w > 0 ? w : 0); }, 0), [items], ); const schedulingLockedHint = flockSchedulingWeightLockedHint(); const optionCompatIssues = useMemo( () => evaluateFlockOptionCompat({ pickupLocationType: pickupType, deliveryLocationType: deliveryType, packagingTypes: items.map((it) => it.packagingType), vehicleTypes, additionalServices, shipmentWeightLb, }), [ pickupType, deliveryType, items, vehicleTypes, additionalServices, shipmentWeightLb, ], ); useEffect(() => { const pickupSvc = FLOCK_PICKUP_SERVICES.find((s) => s.id === pickupService); if ( pickupSvc && isFlockSchedulingOptionLocked(shipmentWeightLb, pickupSvc.weightLocked) ) { setPickupService("standard_fcfs"); } const deliverySvc = FLOCK_DELIVERY_SERVICES.find( (s) => s.id === deliveryService, ); if ( deliverySvc && isFlockSchedulingOptionLocked(shipmentWeightLb, deliverySvc.weightLocked) ) { setDeliveryService("standard_fcfs"); } if ( FLOCK_PICKUP_CALL_BEFORE_WEIGHT_LOCKED && isFlockSchedulingOptionLocked(shipmentWeightLb, true) && callBeforePickup ) { setCallBeforePickup(false); } }, [ shipmentWeightLb, pickupService, deliveryService, callBeforePickup, ]); const canSubmit = useMemo( () => !disabled && !submitLocked, [disabled, submitLocked], ); const updateItem = (key: string, patch: Partial) => { setItems((rows) => rows.map((row) => (row.key === key ? { ...row, ...patch } : row)), ); }; const toggleService = (id: FlockAdditionalServiceId) => { setAdditionalServices((prev) => { const next = prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id]; if ( isFlockReeferOnlyWithoutTemp({ vehicleTypes, additionalServices: next, }) ) { setLocalError( "仅允许「冷藏车」时须同时勾选「温控 / 冷链」,否则官网无法提交询价", ); } else if ( isFlockTempWithoutReefer({ vehicleTypes, additionalServices: next, }) ) { setLocalError("已勾选「温控 / 冷链」时,允许车型中须包含「冷藏车」"); } else { setLocalError(null); } return next; }); }; const toggleVehicle = (id: FlockVehicleTypeId) => { setVehicleTypes((prev) => { const next = prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id]; if (next.length === 0) { setLocalError("请至少选择一种车型"); return prev.includes(id) ? prev : next; } if ( isFlockReeferOnlyWithoutTemp({ vehicleTypes: next, additionalServices, }) ) { setLocalError( "仅允许「冷藏车」时须同时勾选「温控 / 冷链」,否则官网无法提交询价", ); } else if ( isFlockTempWithoutReefer({ vehicleTypes: next, additionalServices, }) ) { setLocalError("已勾选「温控 / 冷链」时,允许车型中须包含「冷藏车」"); } else if (isFlockSprinterOnly(next)) { setLocalError( "仅选「小型厢车」时易无报价,建议同时勾选厢式货车或干货挂车", ); } else { setLocalError(null); } return next; }); }; const handleSubmit = (e: FormEvent) => { e.preventDefault(); if (!canSubmit) return; setLocalError(null); const nextErr: Record = {}; if (!/^\d{5}(-\d{4})?$/.test(pickupZip.trim())) { nextErr.pickupZip = "提货邮编必填(5 位)"; } if (!/^\d{5}(-\d{4})?$/.test(deliveryZip.trim())) { nextErr.deliveryZip = "派送邮编必填(5 位)"; } const pickupDateError = flockPickupDateValidationMessageFromIso(pickupDate); if (pickupDateError) { nextErr.pickupDate = pickupDateError; } if (vehicleTypes.length === 0) { nextErr.vehicles = "请至少选择一种车型"; } if (flockOptionHasBlock(optionCompatIssues)) { const block = optionCompatIssues.find((i) => i.severity === "block")!; setLocalError(block.message); setErrors(nextErr); return; } const parsedItems: FlockLoggedInQuotePayload["items"] = []; for (let i = 0; i < items.length; i += 1) { const it = items[i]!; const quantity = Number(it.quantity); const lengthIn = Number(it.lengthIn); const widthIn = Number(it.widthIn); const heightIn = Number(it.heightIn); const totalWeightLb = Number(it.totalWeightLb); const lineErr = validateFlockLoggedInCargoLine({ quantity, lengthIn, widthIn, heightIn, totalWeightLb, description: it.description, }); if (lineErr.qty) nextErr[`item-${i}-qty`] = lineErr.qty; if (lineErr.len) nextErr[`item-${i}-len`] = lineErr.len; if (lineErr.wid) nextErr[`item-${i}-wid`] = lineErr.wid; if (lineErr.hei) nextErr[`item-${i}-hei`] = lineErr.hei; if (lineErr.wt) nextErr[`item-${i}-wt`] = lineErr.wt; if (lineErr.desc) nextErr[`item-${i}-desc`] = lineErr.desc; if (Object.keys(nextErr).some((k) => k.startsWith(`item-${i}-`))) { continue; } parsedItems.push({ quantity, packagingType: it.packagingType, lengthIn, widthIn, heightIn, totalWeightLb, freightClass: it.freightClass, description: it.description.trim(), stackable: it.stackable, turnable: it.turnable, }); } const shipmentWeight = parsedItems.reduce( (s, it) => s + it.totalWeightLb, 0, ); if ( parsedItems.length > 0 && shipmentWeight > FLOCK_LIMITS.totalWeightLbMax ) { nextErr.shipmentWeight = FLOCK_VALIDATION_MESSAGES.totalWeight; } if (pickupService === "during_window") { if (!pickupWindowStartTime.trim()) { nextErr.pickupWindowStart = "请选择提货时段开始时间"; } if (!pickupWindowEndTime.trim()) { nextErr.pickupWindowEnd = "请选择提货时段结束时间"; } } if (pickupService === "have_appointment" && !pickupAppointmentTime.trim()) { nextErr.pickupApptTime = "请选择提货预约时间"; } if (deliveryService === "during_window") { if (!deliveryWindowStartTime.trim()) { nextErr.deliveryWindowStart = "请选择派送时段开始时间"; } if (!deliveryWindowEndTime.trim()) { nextErr.deliveryWindowEnd = "请选择派送时段结束时间"; } if (!deliveryWindowDate.trim()) { nextErr.deliveryWindowDate = "请选择派送时段日期"; } } if (deliveryService === "have_appointment") { if (!deliveryAppointmentTime.trim()) { nextErr.deliveryApptTime = "请选择派送预约时间"; } if (!deliveryAppointmentDate.trim()) { nextErr.deliveryApptDate = "请选择派送预约日期"; } } if ( deliveryService === "must_arrive_by" && !deliveryMustArriveByDate.trim() ) { nextErr.deliveryMustArrive = "请选择须到达日期"; } setErrors(nextErr); if (Object.keys(nextErr).length > 0 || parsedItems.length === 0) { const firstMsg = nextErr.pickupZip || nextErr.deliveryZip || nextErr.pickupDate || nextErr.vehicles || nextErr.shipmentWeight || Object.values(nextErr)[0] || "请完善必填项后再生成报价"; setLocalError(firstMsg); return; } onValidSubmit?.({ mode: "quick", pickupDate, pickupZip: pickupZip.trim(), pickupType, pickupLiftgate, pickupInside, pickupPalletJack, deliveryZip: deliveryZip.trim(), deliveryType, deliveryLiftgate, deliveryInside, deliveryPalletJack, items: parsedItems, additionalServices, vehicleTypes, pickupService, deliveryService, pickupWindowStartTime: pickupService === "during_window" ? pickupWindowStartTime : undefined, pickupWindowEndTime: pickupService === "during_window" ? pickupWindowEndTime : undefined, pickupAppointmentTime: pickupService === "have_appointment" ? pickupAppointmentTime : undefined, deliveryWindowStartTime: deliveryService === "during_window" ? deliveryWindowStartTime : undefined, deliveryWindowEndTime: deliveryService === "during_window" ? deliveryWindowEndTime : undefined, deliveryWindowDate: deliveryService === "during_window" ? deliveryWindowDate : undefined, deliveryAppointmentTime: deliveryService === "have_appointment" ? deliveryAppointmentTime : undefined, deliveryAppointmentDate: deliveryService === "have_appointment" ? deliveryAppointmentDate : undefined, deliveryMustArriveByDate: deliveryService === "must_arrive_by" ? deliveryMustArriveByDate : undefined, callForDeliveryAppointment: deliveryService === "must_arrive_by" ? callForDeliveryAppointment : undefined, callBeforePickup: FLOCK_PICKUP_CALL_BEFORE_WEIGHT_LOCKED && isFlockSchedulingOptionLocked(shipmentWeight, true) ? false : callBeforePickup, callBeforeDelivery, additionalInsurance, }); }; return (
创建报价
{optionCompatIssues.length > 0 ? (

选项提示(官网规则 / 易无价组合)

    {optionCompatIssues.map((issue) => (
  • {issue.severity === "block" ? "不可提交:" : ""} {issue.message}
  • ))}
) : null} {/* 提货日期 */}
提货日期
{/* 提货与派送地点 */}
提货与派送地点 } />
setPickupZip(e.target.value)} placeholder="如 60611" className={errors.pickupZip ? inputErrCls : inputCls} inputMode="numeric" />
setDeliveryZip(e.target.value)} placeholder="如 78701" className={errors.deliveryZip ? inputErrCls : inputCls} inputMode="numeric" />
{ setItems((rows) => { if (!rows.length) return [applySavedFreightPreset(newItem(), preset)]; const [first, ...rest] = rows; return [applySavedFreightPreset(first, preset), ...rest]; }); }} />
{/* 货物行 */} {items.map((item, idx) => (
货物 {idx + 1} {items.length > 1 ? ( ) : null}
updateItem(item.key, { quantity: e.target.value }) } className={ errors[`item-${idx}-qty`] ? inputErrCls : inputCls } inputMode="numeric" />
updateItem(item.key, { lengthIn: e.target.value }) } className={ errors[`item-${idx}-len`] ? inputErrCls : inputCls } inputMode="decimal" /> updateItem(item.key, { widthIn: e.target.value }) } className={ errors[`item-${idx}-wid`] ? inputErrCls : inputCls } inputMode="decimal" /> updateItem(item.key, { heightIn: e.target.value }) } className={ errors[`item-${idx}-hei`] ? inputErrCls : inputCls } inputMode="decimal" />
updateItem(item.key, { totalWeightLb: e.target.value }) } className={ errors[`item-${idx}-wt`] ? inputErrCls : inputCls } inputMode="decimal" />
{item.freightClass === "density" ? (
{FLOCK_NMFC_NOTICE}
) : null}
updateItem(item.key, { description: e.target.value }) } className={ errors[`item-${idx}-desc`] ? inputErrCls : inputCls } /> setSavedFreightRevision((n) => n + 1)} />

装载选项

{FLOCK_STACKABLE_TIP}

))} {/* 附加服务 */}
{showAdditional ? ( <>

已选 {servicesSelected} 项

{FLOCK_ADDITIONAL_SERVICES.map((s) => ( toggleService(s.id)} /> ))}
允许使用的车型 · 已选 {vehiclesSelected} 项 } /> {errors.vehicles ? ( {errors.vehicles} ) : null}
{FLOCK_VEHICLE_TYPES.map((v) => ( toggleVehicle(v.id)} /> ))}
) : null}
{/* 调度服务 */}
{showScheduling ? (

灰色选项在整票总重不超过 {FLOCK_LIMITS.schedulingWeightMinLb} lb 时不可用;整票总重超过 {FLOCK_LIMITS.schedulingWeightMinLb} lb 后启用。 {shipmentWeightLb > 0 ? ( 当前总重约 {shipmentWeightLb} lb ) : null}

提货服务

{FLOCK_PICKUP_SERVICES.map((s) => { const locked = isFlockSchedulingOptionLocked( shipmentWeightLb, s.weightLocked, ); return ( ); })}
{pickupService === "during_window" && !isFlockSchedulingOptionLocked(shipmentWeightLb, true) ? (

提货时段是什么时候?

) : null} {pickupService === "have_appointment" && !isFlockSchedulingOptionLocked(shipmentWeightLb, true) ? (

预设提货预约时间是?

) : null} {(() => { const callBeforeLocked = isFlockSchedulingOptionLocked( shipmentWeightLb, FLOCK_PICKUP_CALL_BEFORE_WEIGHT_LOCKED, ); return ( ); })()}

派送服务

{FLOCK_DELIVERY_SERVICES.map((s) => { const locked = isFlockSchedulingOptionLocked( shipmentWeightLb, s.weightLocked, ); return ( ); })}
{deliveryService === "during_window" && !isFlockSchedulingOptionLocked(shipmentWeightLb, true) ? (

派送时段是什么时候?

setDeliveryWindowDate(e.target.value)} className={ errors.deliveryWindowDate ? inputErrCls : inputCls } />
) : null} {deliveryService === "have_appointment" && !isFlockSchedulingOptionLocked(shipmentWeightLb, true) ? (

预设派送预约时间是?

setDeliveryAppointmentDate(e.target.value) } className={ errors.deliveryApptDate ? inputErrCls : inputCls } />
) : null} {deliveryService === "must_arrive_by" && !isFlockSchedulingOptionLocked(shipmentWeightLb, true) ? (

货物需要在哪一天之前送达?

setDeliveryMustArriveByDate(e.target.value) } className={ errors.deliveryMustArrive ? inputErrCls : inputCls } />
) : null}
) : null}
{/* 额外保险 */}
{showInsurance ? (
{!additionalInsurance ? (

{FLOCK_INSURANCE_WAIVE_HINT}

) : null}

{FLOCK_INSURANCE_FOOTER}

) : null}
); }