|
|
/**
|
|
|
* Priority1 询价页 — 动态表单逻辑映射(Feathery / #container-mh)
|
|
|
*
|
|
|
* 用途:RPA / Cursor 在任意分支下知道「下一步填什么、怎么填」。
|
|
|
* 探测来源:scripts/probe-priority1-shipment-types.ts → .rpa/priority1-shipment-probe/
|
|
|
* 最后校验:2026-06-26
|
|
|
*/
|
|
|
|
|
|
import type { Priority1DemoInput } from "@/workers/rpa/priority1/demo-input";
|
|
|
import type {
|
|
|
ExpeditedTrailerType,
|
|
|
FtlAdditionalService,
|
|
|
FtlTrailerType,
|
|
|
Priority1ShipmentType,
|
|
|
} from "@/workers/rpa/priority1/shipment-types";
|
|
|
import {
|
|
|
EXPEDITED_TRAILER,
|
|
|
FTL_ADDITIONAL,
|
|
|
FTL_TRAILER,
|
|
|
OPEN_DECK_SUBTYPES,
|
|
|
SHIPMENT_TYPE_CONFIG,
|
|
|
expeditedLiftgateAvailable,
|
|
|
} from "@/workers/rpa/priority1/shipment-types";
|
|
|
|
|
|
/** 页面根与入口 */
|
|
|
export const PRIORITY1_FORM_META = {
|
|
|
entryUrl: "https://www.priority1.com/instant-freight-quotes/#New%20Step%201",
|
|
|
container: "#container-mh",
|
|
|
submitButton: /GET INSTANT QUOTES/i,
|
|
|
quoteApiPattern: /api-static-2\.feathery\.io\/api\/custom_request/i,
|
|
|
/** Feathery 卡片:禁止 .check(),用 force click;黑底 rgb(0,0,0) = 已选 */
|
|
|
featheryCardRule:
|
|
|
"click({ force: true, noWaitAfter: true }) + 点击卡片文字;以 checkbox.checked 或黑底高亮为准",
|
|
|
} as const;
|
|
|
|
|
|
export type FieldControl =
|
|
|
| "text"
|
|
|
| "select"
|
|
|
| "date"
|
|
|
| "phone"
|
|
|
| "feathery_card"
|
|
|
| "checkbox_label";
|
|
|
|
|
|
export type FormFieldDef = {
|
|
|
/** 逻辑键,对应 demo-input 或分支状态 */
|
|
|
key: string;
|
|
|
/** 页面可见标题(中英) */
|
|
|
label: string;
|
|
|
selector: string;
|
|
|
control: FieldControl;
|
|
|
required: boolean;
|
|
|
/** 下拉/卡片可选值 */
|
|
|
options?: readonly string[];
|
|
|
/** 推荐演示默认值 */
|
|
|
demoDefault?: string;
|
|
|
/** Priority1DemoInput 字段名(若有) */
|
|
|
demoKey?: keyof Priority1DemoInput;
|
|
|
section?: string;
|
|
|
notes?: string;
|
|
|
};
|
|
|
|
|
|
export type FormBranch = {
|
|
|
id: string;
|
|
|
description: string;
|
|
|
/** 父级选择条件,全部满足时追加下列字段 */
|
|
|
when: Record<string, string>;
|
|
|
fields: FormFieldDef[];
|
|
|
};
|
|
|
|
|
|
export type FormStepPlan = {
|
|
|
step: "step1" | "step2";
|
|
|
order: number;
|
|
|
field: FormFieldDef;
|
|
|
/** 本次应写入/选择的值 */
|
|
|
value: string;
|
|
|
/** 给操作员的简短中文指令 */
|
|
|
instruction: string;
|
|
|
};
|
|
|
|
|
|
/** ─── Step 1:三种类型共用 ─── */
|
|
|
export const STEP1_FIELDS: FormFieldDef[] = [
|
|
|
{
|
|
|
key: "shipmentType",
|
|
|
label: "Shipment Type / 运输类型",
|
|
|
selector: "#cb_st_ltl | #cb_st_ftl | #cb_st_exp",
|
|
|
control: "feathery_card",
|
|
|
required: true,
|
|
|
options: ["Less Than Truckload", "Full Truckload", "Expedited"],
|
|
|
section: "Step 1",
|
|
|
notes: "三选一卡片;探测到 checkbox id 为 cb_st_ltl / cb_st_ftl / cb_st_exp(非 cb_st_expedited)",
|
|
|
},
|
|
|
{
|
|
|
key: "originZip",
|
|
|
label: "Pickup ZIP Code / 起运邮编",
|
|
|
selector: "#origin-zip",
|
|
|
control: "text",
|
|
|
required: true,
|
|
|
demoDefault: "32801",
|
|
|
demoKey: "originZip",
|
|
|
},
|
|
|
{
|
|
|
key: "destinationZip",
|
|
|
label: "Delivery ZIP Code / 目的邮编",
|
|
|
selector: "#destination-zip",
|
|
|
control: "text",
|
|
|
required: true,
|
|
|
demoDefault: "28202",
|
|
|
demoKey: "destinationZip",
|
|
|
},
|
|
|
{
|
|
|
key: "shipmentFrequency",
|
|
|
label: "Shipments Per Month / 发货频次",
|
|
|
selector: "#freight-shipment",
|
|
|
control: "select",
|
|
|
required: true,
|
|
|
options: [
|
|
|
"1-5",
|
|
|
"5-10",
|
|
|
"10-30",
|
|
|
"30-100",
|
|
|
"100+",
|
|
|
"One Time Shipment",
|
|
|
],
|
|
|
demoDefault: "One Time Shipment",
|
|
|
demoKey: "shipmentFrequency",
|
|
|
},
|
|
|
{
|
|
|
key: "pickupDate",
|
|
|
label: "Pickup Date / 提货日",
|
|
|
selector: "#pickup-date",
|
|
|
control: "date",
|
|
|
required: true,
|
|
|
demoDefault: "07/14/2026",
|
|
|
demoKey: "pickupDate",
|
|
|
notes: "格式 MM/DD/YYYY",
|
|
|
},
|
|
|
{
|
|
|
key: "email",
|
|
|
label: "Business Email / 商务邮箱",
|
|
|
selector: "#business-email",
|
|
|
control: "text",
|
|
|
required: true,
|
|
|
demoKey: "email",
|
|
|
},
|
|
|
{
|
|
|
key: "phone",
|
|
|
label: "Phone / 电话",
|
|
|
selector: "#client-phone",
|
|
|
control: "phone",
|
|
|
required: true,
|
|
|
demoKey: "phone",
|
|
|
notes: "需先选 United States,再 sequential 输入 10 位数字",
|
|
|
},
|
|
|
];
|
|
|
|
|
|
/** Step 1 各类型哈希回退(按钮提交失败时) */
|
|
|
export const STEP2_HASH_FALLBACK: Record<Priority1ShipmentType, string> = {
|
|
|
ltl: "#V4.1%20Step%202%20-%20LTL",
|
|
|
ftl: "#New%20Step%202%20-%20Truckload",
|
|
|
expedited: "#New%20Step%202%20-%20Expedited",
|
|
|
};
|
|
|
|
|
|
/** Step 2 就绪探测选择器 */
|
|
|
export const STEP2_READY_SELECTOR: Record<Priority1ShipmentType, string> = {
|
|
|
ltl: "#length",
|
|
|
ftl: "#Commodity",
|
|
|
expedited: "#cb_stt_sv",
|
|
|
};
|
|
|
|
|
|
/** ─── LTL Step 2 ─── */
|
|
|
export const LTL_STEP2_FIELDS: FormFieldDef[] = [
|
|
|
{ key: "lengthIn", label: "Length (in)", selector: "#length", control: "text", required: true, demoKey: "lengthIn", demoDefault: "48" },
|
|
|
{ key: "widthIn", label: "Width (in)", selector: "#width", control: "text", required: true, demoKey: "widthIn", demoDefault: "40" },
|
|
|
{ key: "heightIn", label: "Height (in)", selector: "#height", control: "text", required: true, demoKey: "heightIn", demoDefault: "48" },
|
|
|
{ key: "weightLb", label: "Total Weight (lb)", selector: "#weight", control: "text", required: true, demoKey: "weightLb", demoDefault: "500", notes: "单票 LTL 常见 150–1800 lb" },
|
|
|
{
|
|
|
key: "packaging",
|
|
|
label: "What are you shipping? / 包装类型",
|
|
|
selector: "#freight-package",
|
|
|
control: "select",
|
|
|
required: true,
|
|
|
options: ["Pallet", "Crate", "Box", "Bundle"],
|
|
|
demoDefault: "Pallet",
|
|
|
demoKey: "packaging",
|
|
|
},
|
|
|
{ key: "palletCount", label: "Number of Pallets", selector: "#number-of-pallets", control: "text", required: true, demoKey: "palletCount", demoDefault: "2" },
|
|
|
{
|
|
|
key: "freightClass",
|
|
|
label: "Freight Class",
|
|
|
selector: "#freight-type",
|
|
|
control: "select",
|
|
|
required: true,
|
|
|
options: ["Not Sure", "50", "55", "60", "65", "70", "77.5", "85", "92.5", "100", "110", "125", "150", "175", "200", "250", "300", "400", "500"],
|
|
|
demoDefault: "70",
|
|
|
demoKey: "freightClass",
|
|
|
notes: "备用选择器 #freight-class",
|
|
|
},
|
|
|
{
|
|
|
key: "pickupLocation",
|
|
|
label: "Pickup Location Type",
|
|
|
selector: "#PickupLocation",
|
|
|
control: "select",
|
|
|
required: true,
|
|
|
options: [
|
|
|
"Residence / Home Business",
|
|
|
"Business (with loading docks or forklift)",
|
|
|
"Business (no loading dock, no forklift)",
|
|
|
],
|
|
|
demoKey: "pickupLocation",
|
|
|
},
|
|
|
{
|
|
|
key: "pickupServices",
|
|
|
label: "Pickup Accessorials / 提货附加",
|
|
|
selector: "getByLabel",
|
|
|
control: "checkbox_label",
|
|
|
required: false,
|
|
|
options: [
|
|
|
"Liftgate at Pickup",
|
|
|
"Inside Pickup",
|
|
|
"Limited Access Pickup",
|
|
|
"Residential Pickup",
|
|
|
],
|
|
|
demoKey: "pickupServices",
|
|
|
notes: "按标签文本点击,非稳定 id",
|
|
|
},
|
|
|
{
|
|
|
key: "deliveryLocation",
|
|
|
label: "Delivery Location Type",
|
|
|
selector: "#DropoffLocation",
|
|
|
control: "select",
|
|
|
required: true,
|
|
|
options: [
|
|
|
"Residence / Home Business",
|
|
|
"Business (with loading docks or forklift)",
|
|
|
"Business (no loading dock, no forklift)",
|
|
|
],
|
|
|
demoKey: "deliveryLocation",
|
|
|
},
|
|
|
{
|
|
|
key: "deliveryServices",
|
|
|
label: "Delivery Accessorials / 派送附加",
|
|
|
selector: "getByLabel",
|
|
|
control: "checkbox_label",
|
|
|
required: false,
|
|
|
options: [
|
|
|
"Liftgate at Delivery",
|
|
|
"Inside Delivery",
|
|
|
"Delivery Appointment",
|
|
|
"Call Before Delivery",
|
|
|
"Limited Access Delivery",
|
|
|
"Hazardous Material",
|
|
|
"Residential Delivery",
|
|
|
],
|
|
|
demoKey: "deliveryServices",
|
|
|
},
|
|
|
];
|
|
|
|
|
|
/** ─── FTL Step 2:主拖车(四选一,决定后续子表单)─── */
|
|
|
export const FTL_TRAILER_FIELDS: FormFieldDef[] = [
|
|
|
{
|
|
|
key: "ftlTrailer_dry_van",
|
|
|
label: "Dry Van",
|
|
|
selector: "#cb_stt_ftl1",
|
|
|
control: "feathery_card",
|
|
|
required: false,
|
|
|
section: "Select Trailer Type *",
|
|
|
notes: "53ft,最多 26 托盘 / 45000 lb",
|
|
|
},
|
|
|
{
|
|
|
key: "ftlTrailer_open_deck",
|
|
|
label: "Open Deck",
|
|
|
selector: "#cb_stt_ftl2",
|
|
|
control: "feathery_card",
|
|
|
required: false,
|
|
|
section: "Select Trailer Type *",
|
|
|
},
|
|
|
{
|
|
|
key: "ftlTrailer_temperature_controlled",
|
|
|
label: "Temperature Controlled",
|
|
|
selector: "#cb_stt_ftl3",
|
|
|
control: "feathery_card",
|
|
|
required: false,
|
|
|
section: "Select Trailer Type *",
|
|
|
},
|
|
|
{
|
|
|
key: "ftlTrailer_ftl_expedited",
|
|
|
label: "Expedited",
|
|
|
selector: "#cb_stt_ftl4",
|
|
|
control: "feathery_card",
|
|
|
required: false,
|
|
|
section: "Select Trailer Type *",
|
|
|
notes: "FTL 下的 Expedited 子流,与主类型 Expedited 不同",
|
|
|
},
|
|
|
];
|
|
|
|
|
|
/** FTL 条件分支:选中主拖车后出现 */
|
|
|
export const FTL_CONDITIONAL_BRANCHES: FormBranch[] = [
|
|
|
{
|
|
|
id: "ftl_open_deck",
|
|
|
description: "Open Deck → 子类型下拉",
|
|
|
when: { ftlTrailer: "open_deck" },
|
|
|
fields: [
|
|
|
{
|
|
|
key: "openDeckSubtype",
|
|
|
label: "Open Deck Type",
|
|
|
selector: "select:has(option:Flatbed)",
|
|
|
control: "select",
|
|
|
required: true,
|
|
|
options: OPEN_DECK_SUBTYPES,
|
|
|
demoDefault: "48 Foot Flatbed",
|
|
|
demoKey: "openDeckSubtype",
|
|
|
notes:
|
|
|
"选中 Open Deck 后出现;实现见 form-interactions.selectOpenDeckSubtype(原生 select → combobox → 文本点击)",
|
|
|
},
|
|
|
],
|
|
|
},
|
|
|
{
|
|
|
id: "ftl_temp",
|
|
|
description: "Temperature Controlled → 温控区间",
|
|
|
when: { ftlTrailer: "temperature_controlled" },
|
|
|
fields: [
|
|
|
{
|
|
|
key: "tempMinF",
|
|
|
label: "Minimum Temperature (°F)",
|
|
|
selector: "#minimum-temperature",
|
|
|
control: "text",
|
|
|
required: true,
|
|
|
demoDefault: "32",
|
|
|
demoKey: "tempMinF",
|
|
|
notes: "录制确认 id=#minimum-temperature(非 placeholder)",
|
|
|
},
|
|
|
{
|
|
|
key: "tempMaxF",
|
|
|
label: "Maximum Temperature (°F)",
|
|
|
selector: "#maximum-temperature",
|
|
|
control: "text",
|
|
|
required: true,
|
|
|
demoDefault: "40",
|
|
|
demoKey: "tempMaxF",
|
|
|
notes: "录制确认 id=#maximum-temperature",
|
|
|
},
|
|
|
],
|
|
|
},
|
|
|
{
|
|
|
id: "ftl_expedited_sub",
|
|
|
description: "FTL Expedited 拖车 → 三选一子车型;Large Straight 时可选 Liftgate",
|
|
|
when: { ftlTrailer: "ftl_expedited" },
|
|
|
fields: [
|
|
|
{
|
|
|
key: "ftlExpeditedTrailer_sprinter",
|
|
|
label: "Sprinter Van",
|
|
|
selector: "#cb_stt_sv",
|
|
|
control: "feathery_card",
|
|
|
required: true,
|
|
|
section: "Select Expedited Trailer Type",
|
|
|
},
|
|
|
{
|
|
|
key: "ftlExpeditedTrailer_small",
|
|
|
label: "Small Straight Box Truck",
|
|
|
selector: "#cb_stt_ssbt",
|
|
|
control: "feathery_card",
|
|
|
required: false,
|
|
|
section: "Select Expedited Trailer Type",
|
|
|
},
|
|
|
{
|
|
|
key: "ftlExpeditedTrailer_large",
|
|
|
label: "Large Straight Box Truck",
|
|
|
selector: "#cb_stt_lsbt",
|
|
|
control: "feathery_card",
|
|
|
required: false,
|
|
|
section: "Select Expedited Trailer Type",
|
|
|
},
|
|
|
{
|
|
|
key: "expeditedLiftgate",
|
|
|
label: "Liftgate",
|
|
|
selector: "text=Liftgate",
|
|
|
control: "checkbox_label",
|
|
|
required: false,
|
|
|
demoKey: "expeditedLiftgate",
|
|
|
},
|
|
|
],
|
|
|
},
|
|
|
];
|
|
|
|
|
|
/** FTL 附加服务(可选,多选) */
|
|
|
export const FTL_ADDITIONAL_FIELDS: FormFieldDef[] = [
|
|
|
{
|
|
|
key: "full_ftl",
|
|
|
label: "Full FTL",
|
|
|
selector: "#cb_as_ftl1",
|
|
|
control: "feathery_card",
|
|
|
required: false,
|
|
|
section: "Additional Services (Optional)",
|
|
|
notes: "实现见 form-interactions.selectFtlAdditionalService;checkbox 用 #container-mh 根定位,勿嵌套 section",
|
|
|
},
|
|
|
{
|
|
|
key: "partial_ptl",
|
|
|
label: "Partial PTL",
|
|
|
selector: "#cb_as_ftl2",
|
|
|
control: "feathery_card",
|
|
|
required: false,
|
|
|
section: "Additional Services (Optional)",
|
|
|
notes: "同上 selectFtlAdditionalService",
|
|
|
},
|
|
|
{
|
|
|
key: "team_drivers",
|
|
|
label: "Team Drivers",
|
|
|
selector: "#cb_as_ftl3",
|
|
|
control: "feathery_card",
|
|
|
required: false,
|
|
|
section: "Additional Services (Optional)",
|
|
|
notes: "同上 selectFtlAdditionalService",
|
|
|
},
|
|
|
];
|
|
|
|
|
|
/** FTL / Expedited 共用货描与重量 */
|
|
|
export const FTL_EXPEDITED_CARGO_FIELDS: FormFieldDef[] = [
|
|
|
{
|
|
|
key: "commodity",
|
|
|
label: "What are you shipping? / 货描",
|
|
|
selector: "#Commodity",
|
|
|
control: "text",
|
|
|
required: true,
|
|
|
demoDefault: "Electronics",
|
|
|
demoKey: "commodity",
|
|
|
notes: "自由文本;演示默认填 Electronics",
|
|
|
},
|
|
|
{
|
|
|
key: "weightLb",
|
|
|
label: "Total Weight (lb)",
|
|
|
selector: "#weight",
|
|
|
control: "text",
|
|
|
required: true,
|
|
|
demoKey: "weightLb",
|
|
|
notes: "FTL Dry Van 演示 12000–18000;Expedited Sprinter ≤3500",
|
|
|
},
|
|
|
];
|
|
|
|
|
|
export const FTL_ONLY_CARGO_FIELDS: FormFieldDef[] = [
|
|
|
{
|
|
|
key: "palletCount",
|
|
|
label: "Number of Pallets",
|
|
|
selector: "#Pallets",
|
|
|
control: "text",
|
|
|
required: true,
|
|
|
demoKey: "palletCount",
|
|
|
demoDefault: "18",
|
|
|
notes: "Dry Van 上限约 26 托盘",
|
|
|
},
|
|
|
];
|
|
|
|
|
|
/** ─── Expedited 主类型 Step 2(无四卡 FTL 拖车步)─── */
|
|
|
export const EXPEDITED_TRAILER_FIELDS: FormFieldDef[] = [
|
|
|
{
|
|
|
key: "expedited_sprinter",
|
|
|
label: "Sprinter Van",
|
|
|
selector: "#cb_stt_sv",
|
|
|
control: "feathery_card",
|
|
|
required: false,
|
|
|
section: "Select Trailer Type *",
|
|
|
notes: "最大约 3500 lb / 3 托盘",
|
|
|
},
|
|
|
{
|
|
|
key: "expedited_small",
|
|
|
label: "Small Straight Box Truck",
|
|
|
selector: "#cb_stt_ssbt",
|
|
|
control: "feathery_card",
|
|
|
required: false,
|
|
|
section: "Select Trailer Type *",
|
|
|
},
|
|
|
{
|
|
|
key: "expedited_large",
|
|
|
label: "Large Straight Box Truck",
|
|
|
selector: "#cb_stt_lsbt",
|
|
|
control: "feathery_card",
|
|
|
required: false,
|
|
|
section: "Select Trailer Type *",
|
|
|
},
|
|
|
];
|
|
|
|
|
|
/** 按拖车推荐重量上限(lb) */
|
|
|
export const WEIGHT_HINTS: Record<string, { min: number; max: number; pallets?: string }> = {
|
|
|
"ftl:dry_van": { min: 8000, max: 42000, pallets: "12–26" },
|
|
|
"ftl:open_deck": { min: 5000, max: 45000, pallets: "8–22" },
|
|
|
"ftl:temperature_controlled": { min: 5000, max: 40000, pallets: "10–24" },
|
|
|
"expedited:sprinter_van": { min: 200, max: 3500, pallets: "1–3" },
|
|
|
"expedited:small_straight": { min: 500, max: 8000, pallets: "4–8" },
|
|
|
"expedited:large_straight": { min: 1000, max: 10000, pallets: "8–14" },
|
|
|
};
|
|
|
|
|
|
function readDemoValue(
|
|
|
demo: Priority1DemoInput,
|
|
|
field: FormFieldDef,
|
|
|
): string {
|
|
|
if (field.demoKey) {
|
|
|
const raw = demo[field.demoKey];
|
|
|
if (Array.isArray(raw)) return raw.join(",");
|
|
|
if (typeof raw === "boolean") return raw ? "true" : "false";
|
|
|
if (raw != null && raw !== "") return String(raw);
|
|
|
}
|
|
|
return field.demoDefault ?? "";
|
|
|
}
|
|
|
|
|
|
function instructionFor(field: FormFieldDef, value: string): string {
|
|
|
switch (field.control) {
|
|
|
case "feathery_card":
|
|
|
return `在「${field.section ?? field.label}」点击卡片「${value}」,确认黑底高亮`;
|
|
|
case "select":
|
|
|
return `下拉 ${field.selector} 选择「${value}」`;
|
|
|
case "checkbox_label":
|
|
|
return value.includes(",")
|
|
|
? `勾选标签:${value}`
|
|
|
: `点击标签「${value}」勾选`;
|
|
|
case "phone":
|
|
|
return `选 US 区号后输入 ${value}`;
|
|
|
default:
|
|
|
return `在 ${field.selector} 填入「${value}」`;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/** 从 Priority1DemoInput 生成完整填写计划(决策树展开) */
|
|
|
export function buildPriority1FormPlan(
|
|
|
demo: Priority1DemoInput,
|
|
|
): FormStepPlan[] {
|
|
|
const plan: FormStepPlan[] = [];
|
|
|
let order = 0;
|
|
|
|
|
|
const push = (step: "step1" | "step2", field: FormFieldDef, value: string) => {
|
|
|
if (!value && field.required) return;
|
|
|
order += 1;
|
|
|
plan.push({
|
|
|
step,
|
|
|
order,
|
|
|
field,
|
|
|
value,
|
|
|
instruction: instructionFor(field, value),
|
|
|
});
|
|
|
};
|
|
|
|
|
|
// Step 1
|
|
|
const stCfg = SHIPMENT_TYPE_CONFIG[demo.shipmentType];
|
|
|
push("step1", STEP1_FIELDS[0]!, stCfg.displayName.split("(")[0]!.trim());
|
|
|
for (const f of STEP1_FIELDS.slice(1)) {
|
|
|
push("step1", f, readDemoValue(demo, f));
|
|
|
}
|
|
|
|
|
|
// Step 2 — 按类型分支
|
|
|
if (demo.shipmentType === "ltl") {
|
|
|
for (const f of LTL_STEP2_FIELDS) {
|
|
|
if (f.key === "pickupServices" || f.key === "deliveryServices") {
|
|
|
const list =
|
|
|
f.key === "pickupServices" ? demo.pickupServices : demo.deliveryServices;
|
|
|
for (const svc of list) push("step2", f, svc);
|
|
|
continue;
|
|
|
}
|
|
|
push("step2", f, readDemoValue(demo, f));
|
|
|
}
|
|
|
return plan;
|
|
|
}
|
|
|
|
|
|
if (demo.shipmentType === "ftl") {
|
|
|
const trailerMeta = FTL_TRAILER[demo.ftlTrailer];
|
|
|
push(
|
|
|
"step2",
|
|
|
{
|
|
|
key: "ftlTrailer",
|
|
|
label: trailerMeta.label,
|
|
|
selector: trailerMeta.selector,
|
|
|
control: "feathery_card",
|
|
|
required: true,
|
|
|
section: "Select Trailer Type *",
|
|
|
demoKey: "ftlTrailer",
|
|
|
},
|
|
|
trailerMeta.label,
|
|
|
);
|
|
|
|
|
|
const branch = FTL_CONDITIONAL_BRANCHES.find(
|
|
|
(b) => b.when.ftlTrailer === demo.ftlTrailer,
|
|
|
);
|
|
|
if (branch) {
|
|
|
for (const f of branch.fields) {
|
|
|
if (f.key.startsWith("ftlExpeditedTrailer_")) {
|
|
|
const sub = EXPEDITED_TRAILER[demo.ftlExpeditedTrailer];
|
|
|
if (f.label === sub.label) push("step2", f, sub.label);
|
|
|
continue;
|
|
|
}
|
|
|
if (
|
|
|
f.key === "expeditedLiftgate" &&
|
|
|
demo.expeditedLiftgate &&
|
|
|
expeditedLiftgateAvailable(demo.ftlExpeditedTrailer)
|
|
|
) {
|
|
|
push("step2", f, "Liftgate");
|
|
|
continue;
|
|
|
}
|
|
|
if (f.key === "expeditedLiftgate") continue;
|
|
|
push("step2", f, readDemoValue(demo, f));
|
|
|
}
|
|
|
}
|
|
|
|
|
|
for (const svc of demo.ftlAdditionalServices) {
|
|
|
const meta = FTL_ADDITIONAL[svc];
|
|
|
const def = FTL_ADDITIONAL_FIELDS.find((x) => x.key === svc);
|
|
|
if (def) push("step2", def, meta.label);
|
|
|
}
|
|
|
|
|
|
for (const f of FTL_EXPEDITED_CARGO_FIELDS) {
|
|
|
push("step2", f, readDemoValue(demo, f));
|
|
|
}
|
|
|
if (demo.ftlTrailer !== "ftl_expedited") {
|
|
|
for (const f of FTL_ONLY_CARGO_FIELDS) {
|
|
|
push("step2", f, readDemoValue(demo, f));
|
|
|
}
|
|
|
}
|
|
|
return plan;
|
|
|
}
|
|
|
|
|
|
// expedited
|
|
|
const expMeta = EXPEDITED_TRAILER[demo.expeditedTrailer];
|
|
|
push(
|
|
|
"step2",
|
|
|
{
|
|
|
key: "expeditedTrailer",
|
|
|
label: expMeta.label,
|
|
|
selector: expMeta.selector,
|
|
|
control: "feathery_card",
|
|
|
required: true,
|
|
|
section: "Select Trailer Type *",
|
|
|
demoKey: "expeditedTrailer",
|
|
|
},
|
|
|
expMeta.label,
|
|
|
);
|
|
|
for (const f of FTL_EXPEDITED_CARGO_FIELDS) {
|
|
|
push("step2", f, readDemoValue(demo, f));
|
|
|
}
|
|
|
if (
|
|
|
demo.expeditedLiftgate &&
|
|
|
expeditedLiftgateAvailable(demo.expeditedTrailer)
|
|
|
) {
|
|
|
push("step2", {
|
|
|
key: "expeditedLiftgate",
|
|
|
label: "Liftgate",
|
|
|
selector: "text=Liftgate",
|
|
|
control: "checkbox_label",
|
|
|
required: false,
|
|
|
}, "Liftgate");
|
|
|
}
|
|
|
|
|
|
return plan;
|
|
|
}
|
|
|
|
|
|
/** 给定当前状态,返回「下一批」应处理的字段(模拟动态表单) */
|
|
|
export function getNextFields(
|
|
|
state: {
|
|
|
shipmentType: Priority1ShipmentType;
|
|
|
step: 1 | 2;
|
|
|
ftlTrailer?: FtlTrailerType;
|
|
|
ftlAdditionalDone?: boolean;
|
|
|
cargoDone?: boolean;
|
|
|
},
|
|
|
): FormFieldDef[] {
|
|
|
if (state.step === 1) return STEP1_FIELDS;
|
|
|
|
|
|
if (state.shipmentType === "ltl") return LTL_STEP2_FIELDS;
|
|
|
|
|
|
if (state.shipmentType === "expedited") {
|
|
|
if (!state.cargoDone) {
|
|
|
return [...EXPEDITED_TRAILER_FIELDS, ...FTL_EXPEDITED_CARGO_FIELDS];
|
|
|
}
|
|
|
return [];
|
|
|
}
|
|
|
|
|
|
// ftl
|
|
|
if (!state.ftlTrailer) return FTL_TRAILER_FIELDS;
|
|
|
const branch = FTL_CONDITIONAL_BRANCHES.find(
|
|
|
(b) => b.when.ftlTrailer === state.ftlTrailer,
|
|
|
);
|
|
|
const cargoFields =
|
|
|
state.ftlTrailer === "ftl_expedited"
|
|
|
? FTL_EXPEDITED_CARGO_FIELDS
|
|
|
: [...FTL_EXPEDITED_CARGO_FIELDS, ...FTL_ONLY_CARGO_FIELDS];
|
|
|
const tail = [...FTL_ADDITIONAL_FIELDS, ...cargoFields];
|
|
|
return branch ? [...branch.fields, ...tail] : tail;
|
|
|
}
|
|
|
|
|
|
/** 场景速查:选 A → 接下来填什么 */
|
|
|
export const SCENARIO_CHEATSHEET: Array<{
|
|
|
path: string;
|
|
|
steps: string[];
|
|
|
}> = [
|
|
|
{
|
|
|
path: "LTL 标准单票",
|
|
|
steps: [
|
|
|
"Step1 选 Less Than Truckload → ZIP/日期/联系信息 → GET INSTANT QUOTES",
|
|
|
"Step2 尺寸 L×W×H → Total Weight → Packaging=Pallet → #托盘 → Freight Class",
|
|
|
"Pickup/Dropoff 地点类型 → 可选附加勾选 → 提交报价",
|
|
|
],
|
|
|
},
|
|
|
{
|
|
|
path: "FTL Dry Van + Full FTL",
|
|
|
steps: [
|
|
|
"Step1 选 Full Truckload",
|
|
|
"Step2 选 Dry Van 卡片 →(无子表单)",
|
|
|
"Additional Services 可点 Full FTL / Partial PTL / Team Drivers",
|
|
|
"What are you shipping?=Pallet → Weight≈15000 → Pallets≈18 → 提交",
|
|
|
],
|
|
|
},
|
|
|
{
|
|
|
path: "FTL Open Deck",
|
|
|
steps: [
|
|
|
"选 Open Deck → 出现 Open Deck Type 下拉(Flatbed/Step Deck/Conestoga/Goose Neck RGN)",
|
|
|
"再填货描/重量/托盘",
|
|
|
],
|
|
|
},
|
|
|
{
|
|
|
path: "FTL Temperature Controlled",
|
|
|
steps: ["选 Temperature Controlled → Min/Max °F → 货描/重量/托盘"],
|
|
|
},
|
|
|
{
|
|
|
path: "FTL 内嵌 Expedited",
|
|
|
steps: [
|
|
|
"选 Expedited(第四张 FTL 卡)→ Select Expedited Trailer Type 三选一",
|
|
|
"Large Straight 时可选 Liftgate → 货描/重量(无 Number of Pallets 字段)",
|
|
|
],
|
|
|
},
|
|
|
{
|
|
|
path: "Expedited 主类型",
|
|
|
steps: [
|
|
|
"Step1 选 Expedited(#cb_st_exp)",
|
|
|
"Step2 直接三卡:Sprinter / Small Straight / Large Straight",
|
|
|
"货描 + Total Weight(无托盘字段);Large Straight 才出现尾板附加服务",
|
|
|
],
|
|
|
},
|
|
|
];
|
|
|
|
|
|
/** 全字段可选值目录(供 RPA / 文档引用) */
|
|
|
export function collectFieldOptionsCatalog(): Array<{
|
|
|
step: string;
|
|
|
key: string;
|
|
|
label: string;
|
|
|
options: string[];
|
|
|
notes?: string;
|
|
|
}> {
|
|
|
const rows: Array<{
|
|
|
step: string;
|
|
|
key: string;
|
|
|
label: string;
|
|
|
options: string[];
|
|
|
notes?: string;
|
|
|
}> = [];
|
|
|
|
|
|
const add = (step: string, fields: FormFieldDef[]) => {
|
|
|
for (const f of fields) {
|
|
|
if (!f.options?.length) continue;
|
|
|
rows.push({
|
|
|
step,
|
|
|
key: f.key,
|
|
|
label: f.label,
|
|
|
options: [...f.options],
|
|
|
notes: f.notes,
|
|
|
});
|
|
|
}
|
|
|
};
|
|
|
|
|
|
add("Step 1", STEP1_FIELDS);
|
|
|
add("Step 2 LTL", LTL_STEP2_FIELDS);
|
|
|
add("Step 2 FTL 拖车", FTL_TRAILER_FIELDS);
|
|
|
add("Step 2 FTL 附加", FTL_ADDITIONAL_FIELDS);
|
|
|
add("Step 2 Expedited 拖车", EXPEDITED_TRAILER_FIELDS);
|
|
|
for (const b of FTL_CONDITIONAL_BRANCHES) {
|
|
|
add(`Step 2 FTL 分支/${b.id}`, b.fields);
|
|
|
}
|
|
|
|
|
|
return rows;
|
|
|
}
|
|
|
|
|
|
/** 导出为 Markdown 参考(可写入文件或打印) */
|
|
|
export function formatFormLogicReference(
|
|
|
demo?: Priority1DemoInput,
|
|
|
opts?: { includeHeader?: boolean; sectionTitle?: string },
|
|
|
): string {
|
|
|
const includeHeader = opts?.includeHeader ?? true;
|
|
|
const lines: string[] = [];
|
|
|
|
|
|
if (includeHeader) {
|
|
|
lines.push(
|
|
|
"# Priority1 动态表单逻辑映射",
|
|
|
"",
|
|
|
`入口:${PRIORITY1_FORM_META.entryUrl}`,
|
|
|
`容器:${PRIORITY1_FORM_META.container}`,
|
|
|
"",
|
|
|
"## 决策规则",
|
|
|
`- ${PRIORITY1_FORM_META.featheryCardRule}`,
|
|
|
"- Step1 提交失败时用 STEP2_HASH_FALLBACK 哈希导航",
|
|
|
"",
|
|
|
"## 场景速查",
|
|
|
);
|
|
|
|
|
|
for (const s of SCENARIO_CHEATSHEET) {
|
|
|
lines.push(`### ${s.path}`);
|
|
|
for (const step of s.steps) lines.push(`- ${step}`);
|
|
|
lines.push("");
|
|
|
}
|
|
|
}
|
|
|
|
|
|
if (demo) {
|
|
|
const title = opts?.sectionTitle ?? "当前输入展开计划";
|
|
|
lines.push(`## ${title}`);
|
|
|
lines.push("```");
|
|
|
for (const row of buildPriority1FormPlan(demo)) {
|
|
|
lines.push(
|
|
|
`${row.order}. [${row.step}] ${row.field.label} ← ${row.value}`,
|
|
|
);
|
|
|
lines.push(` ${row.instruction}`);
|
|
|
}
|
|
|
lines.push("```");
|
|
|
}
|
|
|
|
|
|
return lines.join("\n");
|
|
|
}
|
|
|
|
|
|
export type Priority1PathRef = {
|
|
|
id: string;
|
|
|
title: string;
|
|
|
persona: string;
|
|
|
input: Priority1DemoInput;
|
|
|
verified: "recorded" | "logic-only";
|
|
|
};
|
|
|
|
|
|
/** 完整参考:字段目录 + 路径矩阵 + 各 canonical 路径展开 */
|
|
|
export function formatCompleteFormLogicReference(
|
|
|
paths: Priority1PathRef[],
|
|
|
): string {
|
|
|
const lines: string[] = [
|
|
|
"# Priority1 动态表单逻辑映射",
|
|
|
"",
|
|
|
`入口:${PRIORITY1_FORM_META.entryUrl}`,
|
|
|
`容器:${PRIORITY1_FORM_META.container}`,
|
|
|
"",
|
|
|
"## 决策规则",
|
|
|
`- ${PRIORITY1_FORM_META.featheryCardRule}`,
|
|
|
"- Step1 提交失败时用 STEP2_HASH_FALLBACK 哈希导航",
|
|
|
"",
|
|
|
"## 录制说明",
|
|
|
"- 本文件由 `npm run export:priority1-form-reference` 从 `form-logic-map.ts` + `demo-input.ts` 自动生成,**请勿手改**",
|
|
|
"- **不能**仅靠本文件自动录制;需对每条 `logic-only` 路径执行 `npm run record:priority1` 手动走一遍",
|
|
|
"- 全矩阵批量:`benchmark:priority1-canonical-matrix`(24 条 canonical)",
|
|
|
"- 随机批量:`benchmark:priority1-10cases`(LTL)、`benchmark:priority1-ftl-expedited`",
|
|
|
"",
|
|
|
"## 场景速查(6 种表单结构)",
|
|
|
];
|
|
|
|
|
|
for (const s of SCENARIO_CHEATSHEET) {
|
|
|
lines.push(`### ${s.path}`);
|
|
|
for (const step of s.steps) lines.push(`- ${step}`);
|
|
|
lines.push("");
|
|
|
}
|
|
|
|
|
|
lines.push("## 全字段可选值");
|
|
|
lines.push("");
|
|
|
lines.push("| 步骤 | 字段 | 可选值 |");
|
|
|
lines.push("|------|------|--------|");
|
|
|
for (const row of collectFieldOptionsCatalog()) {
|
|
|
const opts = row.options.join(" / ");
|
|
|
lines.push(`| ${row.step} | ${row.label} | ${opts} |`);
|
|
|
}
|
|
|
lines.push("");
|
|
|
|
|
|
lines.push("## 路径矩阵(全部 canonical 用例)");
|
|
|
lines.push("");
|
|
|
lines.push("| ID | 路径 | 用户画像 | 验证状态 |");
|
|
|
lines.push("|----|------|----------|----------|");
|
|
|
for (const p of paths) {
|
|
|
const status = p.verified === "recorded" ? "已录制" : "待录制";
|
|
|
lines.push(`| ${p.id} | ${p.title} | ${p.persona} | ${status} |`);
|
|
|
}
|
|
|
lines.push("");
|
|
|
|
|
|
const recorded = paths.filter((p) => p.verified === "recorded").length;
|
|
|
const pending = paths.length - recorded;
|
|
|
lines.push(
|
|
|
`> 共 **${paths.length}** 条路径:${recorded} 已录制 / ${pending} 待录制。`,
|
|
|
);
|
|
|
lines.push(
|
|
|
"> LTL/FTL/Expedited 的 **选项组合**(如不同 ZIP、重量)不改变表单结构,仅改变字段值;上表覆盖全部 **结构分支** + 代表性选项。",
|
|
|
);
|
|
|
lines.push("");
|
|
|
|
|
|
for (const p of paths) {
|
|
|
lines.push("---");
|
|
|
lines.push("");
|
|
|
lines.push(`## 演示展开:${p.id} — ${p.title}`);
|
|
|
lines.push("");
|
|
|
lines.push(`> ${p.persona}`);
|
|
|
lines.push("");
|
|
|
lines.push(
|
|
|
formatFormLogicReference(p.input, {
|
|
|
includeHeader: false,
|
|
|
sectionTitle: "填写步骤",
|
|
|
}),
|
|
|
);
|
|
|
lines.push("");
|
|
|
}
|
|
|
|
|
|
lines.push(
|
|
|
"> 源逻辑:`form-logic-map.ts` · 用例:`demo-input.ts` → `PRIORITY1_CANONICAL_PATHS`",
|
|
|
);
|
|
|
|
|
|
return lines.join("\n");
|
|
|
}
|