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.
chajia/components/mothership/mothership-logged-in-detail...

1032 lines
34 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

/**
* MotherShip 登录后二级 Details提货 / 送货 / 货物明细
* 字段与一级 Create shipment 一一对应并预填
*/
"use client";
import { useMemo, useState } from "react";
import { CaretDown, Info, Plus, Warning, X } from "@phosphor-icons/react";
import {
MS_CARGO_TYPES,
MS_DEFAULT_READY_TIME,
MS_DELIVERY_ACCESSORIALS,
MS_PICKUP_ACCESSORIALS,
MS_READY_TIMES,
type MothershipLoggedInShipmentPayload,
type MsCargoTypeId,
} from "@/components/mothership/mothership-logged-in-shipment-form";
import { MothershipWeekdayDatePicker } from "@/components/mothership/mothership-weekday-date-picker";
import {
snapMothershipReadyDateToWeekday,
} from "@/lib/mothership/logged-in-constraints";
import {
evaluateMothershipAccessorialCompat,
MS_PICKUP_BLOCKED_ACCESSORIALS,
toggleMothershipAccessorial,
} from "@/lib/mothership/option-compat";
export type MsLocationDetails = {
address: string;
companyName: string;
suite: string;
contactFirst: string;
contactLast: string;
contactEmail: string;
contactPhone: string;
extension: string;
reference: string;
accessorials: string[];
notes: string;
opensAt: string;
closesAt: string;
readyDate: string;
readyTime: string;
};
export type MsDetailsCargoLine = {
key: string;
cargoType: MsCargoTypeId;
quantity: string;
freightClass: string;
weightLb: string;
lengthIn: string;
widthIn: string;
heightIn: string;
pieceCountType: string;
pieceCountQty: string;
description: string;
nmfc: string;
hazmat: boolean;
alcohol: boolean;
tobacco: boolean;
};
export type MothershipLoggedInDetailsState = {
pickup: MsLocationDetails;
delivery: MsLocationDetails;
requestDeliveryAppointment: boolean;
fbaNumber: string;
fbaPoNumber: string;
cargo: MsDetailsCargoLine[];
};
export type MothershipLoggedInDetailsPayload = {
pickup: {
companyName: string;
suite: string;
contactFirst: string;
contactLast: string;
contactEmail: string;
contactPhone: string;
reference: string;
notes: string;
opensAt: string;
closesAt: string;
};
delivery: {
companyName: string;
suite: string;
contactFirst: string;
contactLast: string;
contactEmail: string;
contactPhone: string;
reference: string;
notes: string;
opensAt: string;
closesAt: string;
};
requestDeliveryAppointment: boolean;
fbaNumber: string;
fbaPoNumber: string;
cargo: Array<{
pieceCountType: string;
pieceCountQty: number;
description: string;
nmfc: string;
hazmat: boolean;
alcohol: boolean;
tobacco: boolean;
}>;
};
function RequiredMark() {
return <span className="text-error">*</span>;
}
function FieldLabel({
children,
required,
tip,
}: {
children: React.ReactNode;
required?: boolean;
tip?: boolean;
}) {
return (
<span className="mb-1 flex items-center gap-1 text-xs font-medium text-text-primary">
{children}
{required ? <RequiredMark /> : null}
{tip ? <Info size={12} className="text-text-disabled" /> : null}
</span>
);
}
function TextInput({
value,
onChange,
placeholder,
disabled,
className = "",
}: {
value: string;
onChange: (v: string) => void;
placeholder?: string;
disabled?: boolean;
className?: string;
}) {
return (
<input
disabled={disabled}
value={value}
placeholder={placeholder}
onChange={(e) => onChange(e.target.value)}
className={`h-10 w-full rounded-md border border-border bg-surface px-3 text-sm focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20 disabled:opacity-60 ${className}`}
/>
);
}
function AccessorialSelect({
options,
selected,
disabled,
onChange,
hintPickUp,
side,
}: {
options: readonly { id: string; label: string }[];
selected: string[];
disabled?: boolean;
onChange: (next: string[]) => void;
hintPickUp?: boolean;
side: "pickup" | "delivery";
}) {
const [open, setOpen] = useState(false);
const [hint, setHint] = useState<string | null>(null);
const issues = useMemo(
() => evaluateMothershipAccessorialCompat({ side, selected }),
[side, selected],
);
const summary =
selected.length === 0
? "选择"
: selected.length === 1
? (options.find((o) => o.id === selected[0])?.label ?? "已选择")
: `${selected.length} 项已选`;
return (
<div className="relative">
<div className="mb-1 flex flex-wrap items-center gap-2">
<FieldLabel tip></FieldLabel>
<span className="inline-flex items-center gap-1 rounded bg-warning/15 px-2 py-0.5 text-[11px] text-warning">
<Warning size={12} weight="fill" />
</span>
</div>
<button
type="button"
disabled={disabled}
onClick={() => setOpen((v) => !v)}
className="flex h-10 w-full items-center justify-between rounded-md border border-border px-3 text-left text-sm"
>
<span>{summary}</span>
<CaretDown size={14} />
</button>
{open && !disabled && (
<div className="absolute z-30 mt-1 max-h-64 w-full overflow-auto rounded-md border border-border bg-surface shadow-modal">
<p className="bg-warning/10 px-3 py-2 text-xs text-text-secondary">
{hintPickUp ? "避免提货问题" : "避免送货问题"}
Amazon
</p>
{options.map((opt) => {
const checked = selected.includes(opt.id);
const warn =
opt.id === "liftgate" ||
opt.id === "residential" ||
opt.id === "limitedAccess";
const pickupBlocked =
side === "pickup" &&
Boolean(MS_PICKUP_BLOCKED_ACCESSORIALS[opt.id]);
return (
<label
key={opt.id}
className={[
"flex items-start gap-2 border-b border-border/60 px-3 py-2 text-sm last:border-0",
pickupBlocked && !checked
? "cursor-not-allowed opacity-60"
: "cursor-pointer hover:bg-bg",
].join(" ")}
>
<input
type="checkbox"
className="mt-0.5"
checked={checked}
disabled={pickupBlocked && !checked}
onChange={() => {
const r = toggleMothershipAccessorial({
side,
selected,
id: opt.id,
});
if (!r.applied) {
setHint(r.message ?? null);
return;
}
setHint(r.message ?? null);
onChange(r.next);
}}
/>
<span>
<span className="block">{opt.label}</span>
{pickupBlocked ? (
<span className="text-[11px] text-[#EF4444]">
{MS_PICKUP_BLOCKED_ACCESSORIALS[opt.id]}
</span>
) : warn ? (
<span className="text-[11px] text-warning">
</span>
) : null}
</span>
</label>
);
})}
</div>
)}
{hint ? (
<p className="mt-1 text-[11px] leading-snug text-[#B45309]">{hint}</p>
) : null}
{issues.length > 0 ? (
<ul className="mt-1 space-y-0.5 text-[11px] leading-snug text-[#92400E]">
{issues.map((i) => (
<li
key={i.code}
className={i.severity === "block" ? "text-[#EF4444]" : ""}
>
{i.message}
</li>
))}
</ul>
) : null}
</div>
);
}
function locationFromL1(
address: string,
accessorials: string[],
readyDate: string,
readyTime: string,
): MsLocationDetails {
return {
address,
companyName: "",
suite: "",
contactFirst: "",
contactLast: "",
contactEmail: "",
contactPhone: "",
extension: "",
reference: "",
accessorials: [...accessorials],
notes: "",
opensAt: "",
closesAt: "",
readyDate: snapMothershipReadyDateToWeekday(readyDate),
readyTime,
};
}
function locationFromConfirmed(
confirmed: {
formatted_address: string;
display_label: string;
street: string;
city: string;
state: string;
zip: string;
},
fallbackQuery: string,
accessorials: string[],
readyDate: string,
readyTime: string,
): MsLocationDetails {
const label =
confirmed.formatted_address ||
confirmed.display_label ||
fallbackQuery;
const composed = [confirmed.street, confirmed.city, confirmed.state, confirmed.zip]
.map((p) => p?.trim())
.filter(Boolean)
.join(", ");
return {
...locationFromL1(label || composed || fallbackQuery, accessorials, readyDate, readyTime),
};
}
export function buildDetailsStateFromL1(
payload: MothershipLoggedInShipmentPayload,
): MothershipLoggedInDetailsState {
const wantFba = payload.deliveryAccessorials.includes("fbaAppointment");
return {
pickup: locationFromConfirmed(
payload.pickupConfirmed,
payload.pickupQuery,
payload.pickupAccessorials,
payload.readyDate,
payload.readyTime,
),
delivery: locationFromConfirmed(
payload.deliveryConfirmed,
payload.deliveryQuery,
payload.deliveryAccessorials,
payload.readyDate,
payload.readyTime,
),
requestDeliveryAppointment:
wantFba || payload.deliveryAccessorials.includes("appointment"),
fbaNumber: "",
fbaPoNumber: "",
cargo: payload.cargo.map((c, i) => ({
key: `d-${i}-${c.cargoType}`,
cargoType: c.cargoType,
quantity: String(c.quantity),
freightClass: "",
weightLb: String(c.weightLb),
lengthIn: String(c.lengthIn),
widthIn: String(c.widthIn),
heightIn: String(c.heightIn),
pieceCountType: "",
pieceCountQty: "0",
description: "",
nmfc: "",
hazmat: false,
alcohol: false,
tobacco: false,
})),
};
}
function LocationSection({
title,
value,
disabled,
accessorialOptions,
hintPickUp,
showFreightReady,
onChange,
}: {
title: string;
value: MsLocationDetails;
disabled?: boolean;
accessorialOptions: readonly { id: string; label: string }[];
hintPickUp?: boolean;
showFreightReady?: boolean;
onChange: (next: MsLocationDetails) => void;
}) {
const set = <K extends keyof MsLocationDetails>(
key: K,
v: MsLocationDetails[K],
) => onChange({ ...value, [key]: v });
return (
<section className="rounded-lg border border-border bg-surface p-4 shadow-card md:p-5">
<h3 className="mb-4 text-base font-semibold text-text-primary">{title}</h3>
<div className="space-y-4">
<label className="block">
<FieldLabel required></FieldLabel>
<TextInput
disabled={disabled}
value={value.address}
onChange={(v) => set("address", v)}
placeholder="街道、城市、州、邮编"
/>
</label>
<div className="grid gap-3 md:grid-cols-[1fr_8rem]">
<label className="block">
<FieldLabel required></FieldLabel>
<TextInput
disabled={disabled}
value={value.companyName}
onChange={(v) => set("companyName", v)}
placeholder="Acme Co."
/>
<p className="mt-1 text-[11px] text-text-secondary">
</p>
</label>
<label className="block">
<FieldLabel> / </FieldLabel>
<TextInput
disabled={disabled}
value={value.suite}
onChange={(v) => set("suite", v)}
placeholder="Ste 301"
/>
</label>
</div>
<div className="grid gap-3 sm:grid-cols-2">
<label className="block">
<FieldLabel></FieldLabel>
<TextInput
disabled={disabled}
value={value.contactFirst}
onChange={(v) => set("contactFirst", v)}
placeholder="名"
/>
</label>
<label className="block">
<FieldLabel></FieldLabel>
<TextInput
disabled={disabled}
value={value.contactLast}
onChange={(v) => set("contactLast", v)}
placeholder="姓"
/>
</label>
</div>
<label className="block">
<FieldLabel required></FieldLabel>
<TextInput
disabled={disabled}
value={value.contactEmail}
onChange={(v) => set("contactEmail", v)}
placeholder="email@company.com"
/>
<p className="mt-1 text-[11px] text-text-secondary">
{hintPickUp
? "填写提货方邮箱以接收提单与跟踪更新。"
: "填写收货方邮箱以接收跟踪更新。"}
</p>
</label>
<div className="grid gap-3 md:grid-cols-[1fr_6rem]">
<label className="block">
<FieldLabel required></FieldLabel>
<TextInput
disabled={disabled}
value={value.contactPhone}
onChange={(v) => set("contactPhone", v)}
placeholder="(123) 456-7890"
/>
</label>
<label className="block">
<FieldLabel></FieldLabel>
<TextInput
disabled={disabled}
value={value.extension}
onChange={(v) => set("extension", v)}
placeholder="1234"
/>
</label>
</div>
<div className="grid gap-3 md:grid-cols-2">
<label className="block">
<FieldLabel tip></FieldLabel>
<TextInput
disabled={disabled}
value={value.reference}
onChange={(v) => set("reference", v)}
placeholder="采购单或参考号"
/>
</label>
<AccessorialSelect
options={accessorialOptions}
selected={value.accessorials}
disabled={disabled}
hintPickUp={hintPickUp}
side={hintPickUp ? "pickup" : "delivery"}
onChange={(next) => set("accessorials", next)}
/>
</div>
<label className="block">
<FieldLabel></FieldLabel>
<textarea
disabled={disabled}
value={value.notes}
onChange={(e) => set("notes", e.target.value)}
placeholder="例如:便于司机找到正确位置的关键信息"
rows={2}
className="w-full rounded-md border border-border px-3 py-2 text-sm focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20"
/>
</label>
<div>
<p className="mb-2 text-xs font-medium text-text-primary"></p>
<div className="flex flex-wrap items-end gap-3">
<label className="block">
<FieldLabel required></FieldLabel>
<select
disabled={disabled}
value={value.opensAt}
onChange={(e) => set("opensAt", e.target.value)}
className="h-10 min-w-[8rem] rounded-md border border-border px-2 text-sm"
>
<option value=""></option>
{MS_READY_TIMES.map((t) => (
<option key={t} value={t}>
{t}
</option>
))}
</select>
</label>
<span className="pb-2 text-xs text-text-secondary">EDT</span>
<label className="block">
<FieldLabel required></FieldLabel>
<select
disabled={disabled}
value={value.closesAt}
onChange={(e) => set("closesAt", e.target.value)}
className="h-10 min-w-[8rem] rounded-md border border-border px-2 text-sm"
>
<option value=""></option>
{MS_READY_TIMES.map((t) => (
<option key={t} value={t}>
{t}
</option>
))}
</select>
</label>
<span className="pb-2 text-xs text-text-secondary">EDT</span>
</div>
</div>
{showFreightReady && (
<div>
<FieldLabel required tip>
</FieldLabel>
<div className="mt-1 flex flex-wrap items-end gap-2">
<MothershipWeekdayDatePicker
disabled={disabled}
value={value.readyDate}
onChange={(iso) => set("readyDate", iso)}
className="h-10 min-w-[11rem] rounded-md border border-border bg-surface px-3 text-sm"
/>
<span className="pb-2 text-sm text-text-secondary"></span>
<select
disabled={disabled}
value={value.readyTime || MS_DEFAULT_READY_TIME}
onChange={(e) => set("readyTime", e.target.value)}
className="h-10 min-w-[7.5rem] rounded-md border border-border px-2 text-sm"
>
{MS_READY_TIMES.map((t) => (
<option key={t} value={t}>
{t}
</option>
))}
</select>
<span className="pb-2 text-xs text-text-secondary">EDT</span>
</div>
</div>
)}
</div>
</section>
);
}
export interface MothershipLoggedInDetailsFormProps {
formId?: string;
initial: MothershipLoggedInShipmentPayload;
disabled?: boolean;
onChange?: (state: MothershipLoggedInDetailsState) => void;
onValidSubmit?: (payload: MothershipLoggedInDetailsPayload) => void;
}
export function MothershipLoggedInDetailsForm({
formId = "ms-logged-in-details-form",
initial,
disabled,
onChange,
onValidSubmit,
}: MothershipLoggedInDetailsFormProps) {
const [state, setState] = useState(() => buildDetailsStateFromL1(initial));
const patch = (next: MothershipLoggedInDetailsState) => {
setState(next);
onChange?.(next);
};
const showFba = useMemo(
() =>
state.requestDeliveryAppointment ||
state.delivery.accessorials.includes("fbaAppointment"),
[state],
);
const toPayload = (): MothershipLoggedInDetailsPayload => ({
pickup: {
companyName: state.pickup.companyName.trim(),
suite: state.pickup.suite.trim(),
contactFirst: state.pickup.contactFirst.trim(),
contactLast: state.pickup.contactLast.trim(),
contactEmail: state.pickup.contactEmail.trim(),
contactPhone: state.pickup.contactPhone.trim(),
reference: state.pickup.reference.trim(),
notes: state.pickup.notes.trim(),
opensAt: state.pickup.opensAt.trim(),
closesAt: state.pickup.closesAt.trim(),
},
delivery: {
companyName: state.delivery.companyName.trim(),
suite: state.delivery.suite.trim(),
contactFirst: state.delivery.contactFirst.trim(),
contactLast: state.delivery.contactLast.trim(),
contactEmail: state.delivery.contactEmail.trim(),
contactPhone: state.delivery.contactPhone.trim(),
reference: state.delivery.reference.trim(),
notes: state.delivery.notes.trim(),
opensAt: state.delivery.opensAt.trim(),
closesAt: state.delivery.closesAt.trim(),
},
requestDeliveryAppointment: state.requestDeliveryAppointment,
fbaNumber: state.fbaNumber.trim(),
fbaPoNumber: state.fbaPoNumber.trim(),
cargo: state.cargo.map((line) => ({
pieceCountType: line.pieceCountType.trim(),
pieceCountQty: Number(line.pieceCountQty || 0),
description: line.description.trim(),
nmfc: line.nmfc.trim(),
hazmat: line.hazmat,
alcohol: line.alcohol,
tobacco: line.tobacco,
})),
});
return (
<form
id={formId}
className="space-y-5"
onSubmit={(e) => {
e.preventDefault();
onValidSubmit?.(toPayload());
}}
>
<div>
<p className="text-xs text-text-secondary">
· <span className="font-medium text-primary"></span> ·
</p>
<h3 className="mt-1 text-xl font-semibold text-text-primary">
</h3>
</div>
<LocationSection
title="1. 提货信息"
value={state.pickup}
disabled={disabled}
accessorialOptions={MS_PICKUP_ACCESSORIALS}
hintPickUp
showFreightReady
onChange={(pickup) => patch({ ...state, pickup })}
/>
{showFba && (
<section className="rounded-lg border border-border bg-surface p-4 shadow-card md:p-5">
<label className="flex items-start gap-2">
<input
type="checkbox"
disabled={disabled}
checked={state.requestDeliveryAppointment}
onChange={(e) =>
patch({
...state,
requestDeliveryAppointment: e.target.checked,
})
}
className="mt-1"
/>
<span>
<span className="block text-sm font-semibold text-text-primary">
</span>
<span className="text-xs text-text-secondary">
/
</span>
</span>
</label>
{state.requestDeliveryAppointment && (
<div className="mt-4 rounded-md bg-warning/10 p-4">
<p className="text-sm font-semibold text-text-primary">
Fulfillment by AmazonFBA
</p>
<div className="mt-3 grid gap-3 sm:grid-cols-2">
<label className="block">
<FieldLabel>FBA </FieldLabel>
<TextInput
disabled={disabled}
value={state.fbaNumber}
onChange={(v) => patch({ ...state, fbaNumber: v })}
placeholder="输入 FBA 编号"
/>
</label>
<label className="block">
<FieldLabel></FieldLabel>
<TextInput
disabled={disabled}
value={state.fbaPoNumber}
onChange={(v) => patch({ ...state, fbaPoNumber: v })}
placeholder="输入采购单号"
/>
</label>
</div>
<ul className="mt-3 list-disc space-y-1 pl-5 text-xs text-text-secondary">
<li>FBA </li>
<li>
{" "}
<strong className="text-text-primary">piece count</strong>
</li>
</ul>
</div>
)}
</section>
)}
<LocationSection
title="2. 送货信息"
value={state.delivery}
disabled={disabled}
accessorialOptions={MS_DELIVERY_ACCESSORIALS}
onChange={(delivery) => patch({ ...state, delivery })}
/>
<section className="rounded-lg border border-border bg-surface p-4 shadow-card md:p-5">
<div className="mb-4 flex items-center justify-between gap-3">
<h3 className="text-base font-semibold text-text-primary">3. </h3>
<button
type="button"
disabled={disabled}
onClick={() =>
patch({
...state,
cargo: [
...state.cargo,
{
key: `d-${Date.now()}`,
cargoType: "pallet",
quantity: "",
freightClass: "",
weightLb: "",
lengthIn: "",
widthIn: "",
heightIn: "",
pieceCountType: "",
pieceCountQty: "0",
description: "",
nmfc: "",
hazmat: false,
alcohol: false,
tobacco: false,
},
],
})
}
className="inline-flex h-9 items-center gap-1.5 rounded-md bg-text-primary px-3 text-sm font-medium text-white"
>
<Plus size={14} weight="bold" />
</button>
</div>
<div className="space-y-4">
{state.cargo.map((line, idx) => (
<div
key={line.key}
className="relative rounded-md border border-border p-4"
>
<div className="mb-3 flex items-center justify-between">
<span className="text-sm font-semibold text-text-primary">
{idx + 1}
</span>
<button
type="button"
disabled={disabled || state.cargo.length <= 1}
aria-label="删除货物行"
onClick={() =>
patch({
...state,
cargo: state.cargo.filter((c) => c.key !== line.key),
})
}
className="inline-flex h-8 w-8 items-center justify-center rounded border border-border text-text-secondary disabled:opacity-40"
>
<X size={14} weight="bold" />
</button>
</div>
<div className="grid gap-3 sm:grid-cols-3">
<label className="block">
<FieldLabel required tip>
</FieldLabel>
<select
disabled={disabled}
value={line.cargoType}
onChange={(e) => {
const cargo = state.cargo.map((c) =>
c.key === line.key
? {
...c,
cargoType: e.target.value as MsCargoTypeId,
}
: c,
);
patch({ ...state, cargo });
}}
className="h-10 w-full rounded-md border border-border px-2 text-sm"
>
{MS_CARGO_TYPES.map((t) => (
<option key={t.id} value={t.id}>
{t.label}
</option>
))}
</select>
</label>
<label className="block">
<FieldLabel required></FieldLabel>
<TextInput
disabled={disabled}
value={line.quantity}
onChange={(v) => {
const cargo = state.cargo.map((c) =>
c.key === line.key ? { ...c, quantity: v } : c,
);
patch({ ...state, cargo });
}}
/>
</label>
<label className="block">
<FieldLabel tip></FieldLabel>
<TextInput
disabled={disabled}
value={line.freightClass}
onChange={(v) => {
const cargo = state.cargo.map((c) =>
c.key === line.key ? { ...c, freightClass: v } : c,
);
patch({ ...state, cargo });
}}
placeholder="选择"
/>
</label>
</div>
<div className="mt-3 grid gap-3 sm:grid-cols-4">
{(
[
["weightLb", "单件重量", "lbs"],
["lengthIn", "单件长", "inch"],
["widthIn", "单件宽", "inch"],
["heightIn", "单件高", "inch"],
] as const
).map(([key, label, ph]) => (
<label key={key} className="block">
<FieldLabel required={key !== "weightLb"}>
{label}
</FieldLabel>
<TextInput
disabled={disabled}
value={line[key]}
onChange={(v) => {
const cargo = state.cargo.map((c) =>
c.key === line.key ? { ...c, [key]: v } : c,
);
patch({ ...state, cargo });
}}
placeholder={ph}
/>
</label>
))}
</div>
<div className="mt-3 grid gap-3 sm:grid-cols-2">
<label className="block">
<FieldLabel required tip>
</FieldLabel>
<select
disabled={disabled}
value={line.pieceCountType}
onChange={(e) => {
const cargo = state.cargo.map((c) =>
c.key === line.key
? { ...c, pieceCountType: e.target.value }
: c,
);
patch({ ...state, cargo });
}}
className="h-10 w-full rounded-md border border-border px-2 text-sm"
>
<option value=""></option>
<option value="pieces">Pieces</option>
<option value="cartons">Cartons</option>
<option value="units">Units</option>
</select>
</label>
<label className="block">
<FieldLabel required></FieldLabel>
<TextInput
disabled={disabled}
value={line.pieceCountQty}
onChange={(v) => {
const cargo = state.cargo.map((c) =>
c.key === line.key ? { ...c, pieceCountQty: v } : c,
);
patch({ ...state, cargo });
}}
/>
</label>
</div>
<label className="mt-3 block">
<FieldLabel required></FieldLabel>
<textarea
disabled={disabled}
value={line.description}
onChange={(e) => {
const cargo = state.cargo.map((c) =>
c.key === line.key
? { ...c, description: e.target.value }
: c,
);
patch({ ...state, cargo });
}}
rows={2}
placeholder="准确描述货物以避免查验与加收。例如:用「地毯」而非「一般商品」。"
className="w-full rounded-md border border-border px-3 py-2 text-sm"
/>
</label>
<label className="mt-3 block">
<FieldLabel tip>NMFC </FieldLabel>
<TextInput
disabled={disabled}
value={line.nmfc}
onChange={(v) => {
const cargo = state.cargo.map((c) =>
c.key === line.key ? { ...c, nmfc: v } : c,
);
patch({ ...state, cargo });
}}
placeholder="输入 NMFC 编码"
/>
<p className="mt-1 text-[11px] text-text-secondary">
</p>
</label>
<div className="mt-4">
<p className="mb-2 text-xs font-medium text-text-primary">
</p>
<div className="flex flex-wrap gap-4 text-sm">
{(
[
["hazmat", "危险品Hazmat"],
["alcohol", "啤酒/葡萄酒/烈酒"],
["tobacco", "尼古丁/烟草"],
] as const
).map(([key, label]) => (
<label key={key} className="inline-flex items-center gap-2">
<input
type="checkbox"
disabled={disabled}
checked={line[key]}
onChange={(e) => {
const cargo = state.cargo.map((c) =>
c.key === line.key
? { ...c, [key]: e.target.checked }
: c,
);
patch({ ...state, cargo });
}}
/>
{label}
</label>
))}
</div>
</div>
</div>
))}
</div>
</section>
</form>
);
}