diff --git a/docs/superpowers/plans/2026-09-15-mp-webview-pl-plan-mobile-redesign.md b/docs/superpowers/plans/2026-09-15-mp-webview-pl-plan-mobile-redesign.md new file mode 100644 index 000000000..87e51cf1d --- /dev/null +++ b/docs/superpowers/plans/2026-09-15-mp-webview-pl-plan-mobile-redesign.md @@ -0,0 +1,810 @@ +# 小程序 WebView 运输计划移动端重设计 Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** 按已验收 Demo 重写 `web-admin` `/#/m/pl/plan*` 的移动端 UI/交互,使列表→详情→表单(含安排)→打印→三方人车达到手机可用,且业务规则与桌面一致。 + +**Architecture:** 不改小程序壳与 ticket。在现有 `views/m/pl/plan/*` 上按屏替换 UI;抽出可单测的卡片/底栏辅助函数;新增通用 Bottom Sheet;列表/详情原生重写;表单继续复用 `PlPlanFormDialog`(`variant=page`)并增强锚点与窄屏布局;打印/三方人车对齐 Demo 交互细节。权威对照:`bak/mobile-ui/shanxia-mp-pl-plan-mobile-redesign-demo.html`。 + +**Tech Stack:** Vue 3 + TypeScript、Element Plus(仅必要时)、`mp-webview.scss` 三墙 Token、`planApi` / `pl-plan-action-guards` / `usePlToxicProductCertCheck` / `PlPlanFormDialog` / `PlPasteRecognizeDialog`。 + +**Spec:** `docs/superpowers/specs/2026-09-15-mp-webview-pl-plan-mobile-redesign-design.md` + +--- + +## 文件结构 + +| 路径 | 职责 | +|------|------| +| `web-admin/src/styles/mp-webview.scss` | 扩展:双下拉筛选、Speed Dial、贴底 fab、Sheet、详情分区、产品行表 | +| `web-admin/src/components/mp/MpBottomSheet.vue` | 通用底部抽屉(遮罩无 blur、可选搜索、关闭) | +| `web-admin/src/utils/mp-pl-plan-card.ts` | 卡片字段:供方→需方、跟进人等 | +| `web-admin/src/utils/mp-pl-plan-detail-actions.ts` | 详情底栏:主操作最多 2 个 + 更多列表 | +| `web-admin/src/views/m/pl/plan/index.vue` | 列表重写 | +| `web-admin/src/views/m/pl/plan/detail.vue` | 详情重写(分区 + 危化证 + 底栏) | +| `web-admin/src/components/business/pl-plan-form-dialog/index.vue` | `variant=page`:锚点条、分区滚动、贴底 footer 样式 | +| `web-admin/src/views/m/pl/plan/form.vue` | 路由壳保持;确认 arrange/copy query | +| `web-admin/src/views/m/pl/plan/print.vue` | 送货客户用 Sheet/ picker 风格;空白单 Chip | +| `web-admin/src/views/m/pl/plan/paste.vue` | 保持 page 变体;核对窄屏 footer | +| `web-admin/src/utils/mp-pl-plan-card.spec.ts`(或 vitest 同名) | 卡片字段单测 | +| `web-admin/src/utils/mp-pl-plan-detail-actions.spec.ts` | 底栏拆分单测 | + +**不改(除非发现缺口):** `MpWebviewShell.vue`、ticket API、小程序 `mp-webview`、后端计划业务规则。 + +**对照顺序:** 每完成一屏,用浏览器窄屏(375)打开 `/#/m/pl/...`,并对照 Demo 同名屏。 + +--- + +### Task 1: 扩展 `mp-webview.scss` + `MpBottomSheet` + +**Files:** +- Modify: `web-admin/src/styles/mp-webview.scss` +- Create: `web-admin/src/components/mp/MpBottomSheet.vue` + +- [ ] **Step 1: 在 `mp-webview.scss` 末尾追加 Demo 对齐样式块** + +追加(保持现有 class,勿改坏已有页): + +```scss +/* === redesign 2026-09-15 === */ +.mp-wv-filter-bar { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 0; + background: #fff; + border-bottom: 1px solid #dcebe4; + position: sticky; + top: 0; + z-index: 20; +} +.mp-wv-filter-trig { + display: flex; + align-items: center; + justify-content: center; + gap: 4px; + height: 40px; + font-size: 13px; + color: #1f2a26; + background: #fff; + border: none; + border-right: 1px solid #dcebe4; + cursor: pointer; +} +.mp-wv-filter-trig:last-child { border-right: none; } +.mp-wv-filter-trig.open { color: #0a9f6d; font-weight: 600; } +.mp-wv-filter-panel { + display: none; + position: absolute; + left: 0; + right: 0; + background: #fff; + border-bottom: 1px solid #dcebe4; + max-height: 280px; + overflow: auto; + z-index: 21; +} +.mp-wv-filter-panel.open { display: block; } +.mp-wv-filter-opt { + display: flex; + justify-content: space-between; + width: 100%; + padding: 12px 16px; + font-size: 14px; + text-align: left; + background: #fff; + border: none; + border-bottom: 1px solid #eef4f1; + cursor: pointer; +} +.mp-wv-filter-opt.active { color: #0a9f6d; font-weight: 600; } +.mp-wv-filter-mask { + display: none; + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.35); + z-index: 19; +} +.mp-wv-filter-mask.open { display: block; } + +.mp-wv-speed-dial { + position: fixed; + right: 16px; + bottom: calc(16px + env(safe-area-inset-bottom, 0px)); + z-index: 30; + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 10px; +} +.mp-wv-speed-main { + width: 52px; + height: 52px; + border-radius: 50%; + border: none; + background: #0a9f6d; + color: #fff; + font-size: 28px; + line-height: 1; + box-shadow: 0 8px 20px rgba(10, 159, 109, 0.35); + cursor: pointer; +} +.mp-wv-speed-item { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 12px; + border-radius: 999px; + border: 1px solid #dcebe4; + background: #fff; + font-size: 13px; + cursor: pointer; +} +.mp-wv-speed-item.primary { + background: #0a9f6d; + border-color: #0a9f6d; + color: #fff; +} +.mp-wv-speed-menu { display: none; flex-direction: column; gap: 8px; align-items: flex-end; } +.mp-wv-speed-dial.open .mp-wv-speed-menu { display: flex; } +.mp-wv-speed-mask { + display: none; + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.25); + z-index: 29; +} +.mp-wv-speed-mask.open { display: block; } + +.mp-wv-fab-bar { + position: sticky; + bottom: 0; + display: flex; + gap: 8px; + padding: 10px 12px calc(10px + env(safe-area-inset-bottom, 0px)); + background: #fff; + border-top: 1px solid #dcebe4; + z-index: 15; +} +.mp-wv-fab-bar .mp-wv-btn { flex: 1; } +.mp-wv-fab-bar .mp-wv-btn-more { flex: 0 0 44px; } + +.mp-wv-section { + margin: 0 0 12px; + padding: 12px; + background: #fff; + border: 1px solid #dcebe4; + border-radius: 14px; +} +.mp-wv-section h3 { + margin: 0 0 10px; + font-size: 14px; + font-weight: 700; + color: #087f57; +} +.mp-wv-kv { + display: flex; + justify-content: space-between; + gap: 12px; + padding: 6px 0; + font-size: 13px; + border-bottom: 1px solid #eef4f1; +} +.mp-wv-kv span:first-child { color: #6b7b74; flex-shrink: 0; } +.mp-wv-kv span:last-child { text-align: right; word-break: break-all; } + +.mp-wv-pad-speed { padding-bottom: 88px; } +``` + +- [ ] **Step 2: 创建 `MpBottomSheet.vue`** + +```vue + + + + + + +``` + +- [ ] **Step 3: 浏览器确认样式文件无 SCSS 语法错误** + +```bash +cd web-admin && pnpm exec vite build --mode development 2>&1 | tail -30 +``` + +若全量 build 过慢,至少 `pnpm dev` 启动后打开任意 `/#/m/pl/plan` 看控制台无 SCSS 报错。 + +- [ ] **Step 4: Commit** + +```bash +git add web-admin/src/styles/mp-webview.scss web-admin/src/components/mp/MpBottomSheet.vue +git commit -m "$(cat <<'EOF' +feat(web-admin): add mp webview sheet and list chrome styles. + +Provide shared Bottom Sheet and filter/speed-dial/fab tokens for the transport plan mobile redesign. +EOF +)" +``` + +--- + +### Task 2: 卡片字段辅助函数 + 单测 + +**Files:** +- Modify: `web-admin/src/utils/mp-pl-plan-card.ts` +- Create: `web-admin/src/utils/mp-pl-plan-card.spec.ts`(若仓库无 vitest 前端单测惯例,则用 `node --experimental-vm-modules` 或跳过文件、在 Task 内用临时断言脚本;**优先**检查 `web-admin/package.json` 的 `test` 脚本,有则按项目方式跑) + +- [ ] **Step 1: 扩展 `buildPlanCard` 返回字段** + +```ts +// web-admin/src/utils/mp-pl-plan-card.ts +import type { Plan } from '@/api/pl' +import { + formatPlVehiclePlateDisplay, + getPlPlanFollowStatusLabel, + getPlPlanFollowStatusTagType, + getPlPlanTypeLabel +} from '@/utils/pl-display' + +function productSummary(plan: Plan): string { + const extra = (plan as Plan & { productSummary?: string }).productSummary + if (extra) return extra + const names = (plan.products || []).map((item) => item.productName).filter(Boolean) + if (names.length) return names.join(' · ') + const parties = [plan.supplierName, plan.customerName || plan.destinationName] + .filter(Boolean) + .join(' → ') + return parties || '—' +} + +export function buildPlanCard(plan: Plan) { + const supplier = plan.supplierName || '—' + const buyer = plan.customerName || plan.destinationName || '—' + return { + id: plan.id, + planNo: plan.planNo || '—', + typeLabel: getPlPlanTypeLabel(plan.planType || ''), + planDate: plan.planDate || '—', + statusLabel: getPlPlanFollowStatusLabel(plan.followStatus), + statusTagType: getPlPlanFollowStatusTagType(plan.followStatus), + plate: formatPlVehiclePlateDisplay(plan.licensePlate, plan.trailerNo) || '—', + people: plan.personnelSummary || '—', + product: productSummary(plan), + supplier, + buyer, + partyLine: `${supplier} → ${buyer}`, + follower: plan.followUserName || '—' + } +} +``` + +- [ ] **Step 2: 写单测(字段存在)** + +```ts +// web-admin/src/utils/mp-pl-plan-card.spec.ts +import { describe, expect, it } from 'vitest' +import { buildPlanCard } from './mp-pl-plan-card' +import type { Plan } from '@/api/pl' + +describe('buildPlanCard', () => { + it('exposes supplier buyer follower for list cards', () => { + const card = buildPlanCard({ + id: '1', + supplierName: '安庆仓', + customerName: '合肥某化', + followUserName: '王调度', + planType: 'outbound', + followStatus: 'pending_arrange' + } as Plan) + expect(card.partyLine).toBe('安庆仓 → 合肥某化') + expect(card.follower).toBe('王调度') + }) +}) +``` + +若项目无 vitest:改为在 `web-admin/scripts/check-mp-pl-plan-card.mjs` 用简单 assert,或本 Task 仅手工 `node` 导入检查;**不要空测**。先读 `package.json`: + +```bash +cd web-admin && node -e "const p=require('./package.json'); console.log(p.scripts)" +``` + +有 `test`/`vitest` 则跑对应命令;没有则跳过 `.spec.ts`,在 Step 3 用临时脚本验证 `partyLine` 拼接逻辑(把纯函数抽到同文件 `export function formatPartyLine(s,b){...}` 再测)。 + +- [ ] **Step 3: 跑测** + +```bash +cd web-admin && pnpm exec vitest run src/utils/mp-pl-plan-card.spec.ts +``` + +Expected: PASS(或按项目实际测试命令)。 + +- [ ] **Step 4: Commit** + +```bash +git add web-admin/src/utils/mp-pl-plan-card.ts web-admin/src/utils/mp-pl-plan-card.spec.ts +git commit -m "$(cat <<'EOF' +feat(web-admin): enrich mp plan card with party and follower fields. + +List cards need supplier→buyer and follow user to match the approved mobile demo. +EOF +)" +``` + +--- + +### Task 3: 详情底栏拆分辅助 + 单测 + +**Files:** +- Create: `web-admin/src/utils/mp-pl-plan-detail-actions.ts` +- Create: `web-admin/src/utils/mp-pl-plan-detail-actions.spec.ts` + +- [ ] **Step 1: 实现拆分逻辑** + +```ts +// web-admin/src/utils/mp-pl-plan-detail-actions.ts +export type MpDetailActionKind = + | 'edit' + | 'arrange' + | 'copy' + | 'wait' + | 'cancel-wait' + | 'print' + | 'void' + +export type MpDetailAction = { + kind: MpDetailActionKind + label: string + danger?: boolean +} + +/** 主栏最多 2 个:优先 edit、arrange;其余进 more */ +export function splitMpPlanDetailActions(input: { + canEdit: boolean + canArrange: boolean + arrangeLabel: string + canWait: boolean + canCancelWait: boolean + canVoid: boolean +}): { primary: MpDetailAction[]; more: MpDetailAction[] } { + const primary: MpDetailAction[] = [] + const more: MpDetailAction[] = [] + + if (input.canEdit) primary.push({ kind: 'edit', label: '编辑' }) + if (input.canArrange) { + primary.push({ kind: 'arrange', label: input.arrangeLabel }) + } + + more.push({ kind: 'copy', label: '复制' }) + if (input.canWait) more.push({ kind: 'wait', label: '等客户通知' }) + if (input.canCancelWait) more.push({ kind: 'cancel-wait', label: '取消等通知' }) + more.push({ kind: 'print', label: '打印单据' }) + if (input.canVoid) more.push({ kind: 'void', label: '作废', danger: true }) + + while (primary.length > 2) more.unshift(primary.pop()!) + while (primary.length < 2 && more.length) { + primary.push(more.shift()!) + } + return { primary, more } +} +``` + +- [ ] **Step 2: 单测** + +```ts +import { describe, expect, it } from 'vitest' +import { splitMpPlanDetailActions } from './mp-pl-plan-detail-actions' + +describe('splitMpPlanDetailActions', () => { + it('keeps edit and arrange as primary when both available', () => { + const { primary, more } = splitMpPlanDetailActions({ + canEdit: true, + canArrange: true, + arrangeLabel: '安排运输', + canWait: true, + canCancelWait: false, + canVoid: true + }) + expect(primary.map((a) => a.kind)).toEqual(['edit', 'arrange']) + expect(more.map((a) => a.kind)).toEqual(['copy', 'wait', 'print', 'void']) + }) +}) +``` + +- [ ] **Step 3: 跑测并 Commit** + +```bash +cd web-admin && pnpm exec vitest run src/utils/mp-pl-plan-detail-actions.spec.ts +git add web-admin/src/utils/mp-pl-plan-detail-actions.ts web-admin/src/utils/mp-pl-plan-detail-actions.spec.ts +git commit -m "$(cat <<'EOF' +feat(web-admin): add mp plan detail action splitter. + +Cap the sticky footer at two primary actions and park the rest in a more sheet. +EOF +)" +``` + +--- + +### Task 4: 重写列表页 `index.vue` + +**Files:** +- Modify: `web-admin/src/views/m/pl/plan/index.vue` +- 对照: Demo `#screen-list`;现有 list API 逻辑保留 + +- [ ] **Step 1: 替换模板为双下拉 + 卡片字段 + Speed Dial** + +要点(完整替换 template,保留 `MpWebviewShell` / 无权限空态): + +1. 去掉横向 `mp-wv-chips` + `mp-wv-tabs` + 顶栏「新增」按钮 + 工具栏「三方人车」。 +2. Header 仅:← 工作台、标题、搜索。 +3. `mp-wv-filter-bar`:日期触发器 + Tab 触发器;面板选项来自 `dateOptions` 与 `PL_PLAN_LIST_TABS`。 +4. 日期增加 `custom`:面板内两个 `input type="date"` + 确定,写入 `dateRange`。 +5. 卡片展示 `partyLine`、`follower`;操作区:更多(`MpBottomSheet`:等通知/取消等通知/作废)、复制、安排(`canArrangeTransport` / `arrangeTransportLabel`)。 +6. Speed Dial:新增 → `/m/pl/plan/new`;三方人车 → `/m/pl/plan/paste`。 +7. 保留 `fetchPage` / `loadMore` / `planApi.list` / 403 处理。 + +卡片更多操作需: + +```ts +import { + canArrangeTransport, + arrangeTransportLabel, + canWaitNotice, + canCancelWaitNotice, + canVoidPlan +} from '@/utils/pl-plan-action-guards' +``` + +作废/等通知调用与详情页相同的 `planApi` 方法(从现有 `detail.vue` 复制调用方式,勿发明新 API)。 + +- [ ] **Step 2: 手动验收列表** + +```bash +cd web-admin && pnpm dev +``` + +浏览器开 `/#/m/pl/plan`(需 ticket 或本地已登录 mp 调试方式,与现网一致)。核对: + +- Tab 顺序含「全部」且在草稿后 +- 自定义日期可筛 +- Speed Dial 两项可跳 +- 卡片有供方→需方、跟进人 + +- [ ] **Step 3: Commit** + +```bash +git add web-admin/src/views/m/pl/plan/index.vue +git commit -m "$(cat <<'EOF' +feat(web-admin): redesign mp transport plan list to match demo. + +Use dual dropdown filters, richer cards, and a speed dial for create actions. +EOF +)" +``` + +--- + +### Task 5: 重写详情页 `detail.vue` + +**Files:** +- Modify: `web-admin/src/views/m/pl/plan/detail.vue` +- Reuse: `planApi.get`(或现有 detail 加载)、`PlPlanHazardCertViewTable`、`getRequiredCertSlots` / `isHazardProduct`、`splitMpPlanDetailActions`、`MpBottomSheet` +- 对照: Demo 详情分区;Web `PlPlanViewTabs` 字段语义 + +- [ ] **Step 1: 加载完整计划并分区渲染** + +数据:继续用现有 `planApi` 详情接口(查看 `detail.vue` 当前 `load`;若字段不足,对齐桌面查看所用的 get/detail 调用,**不要**新造后端)。 + +分区(`mp-wv-section`): + +1. 基础:计划编号、计划日期、归属公司、单据类型、跟进人 +2. 运输:已安排时司机链接复制、多现场链接复制、车辆摘要 +「查看」Sheet、回传摘要(有则显示) +3. 装送货:装货 kv + 产品行;送货段 Tab 切换;数量平衡(用计划上已有汇总或本地加总 planQty) +4. 危化:`` + 齐备 Tag(`isHazardCertReady` 若 API 有字段) +5. 运费:只读 kv(是否有运费、公里、方式、量、单价、金额、开票、备注)— 无运费时其余为 — +6. 备注 + +底栏:`splitMpPlanDetailActions` → ⋯ 打开 `MpBottomSheet`;主按钮绑定 edit/arrange/copy/wait/void/print 导航与现有 API。 + +导航: + +```ts +router.push(`/m/pl/plan/${id}/edit`) +router.push(`/m/pl/plan/${id}/arrange`) +router.push(`/m/pl/plan/new?copyFrom=${id}`) +router.push(`/m/pl/plan/${id}/print`) +``` + +- [ ] **Step 2: 窄屏验收详情** + +打开已安排单与待安排单各一条:多送货段 Tab、危化槽位、底栏仅 2 主按钮 + 更多。 + +- [ ] **Step 3: Commit** + +```bash +git add web-admin/src/views/m/pl/plan/detail.vue +git commit -m "$(cat <<'EOF' +feat(web-admin): redesign mp transport plan detail sections. + +Align mobile detail with demo IA including hazard cert slots and capped footer actions. +EOF +)" +``` + +--- + +### Task 6: 表单 page 变体对齐 Demo(锚点 + 贴底) + +**Files:** +- Modify: `web-admin/src/components/business/pl-plan-form-dialog/index.vue`(仅增强 `isPageVariant` 分支的模板/样式,**不改**提交/校验核心) +- Modify: `web-admin/src/views/m/pl/plan/form.vue`(如需:`copyFrom` query、标题) +- 对照: Demo 表单锚点与底栏四按钮文案(已由 dialog 逻辑控制) + +- [ ] **Step 1: page 变体增加锚点条** + +在 `isPageVariant` 时,于 `.mp-plan-form-title` 下增加: + +```vue + +``` + +```ts +const mpAnchors = [ + { id: 'sec-basic', label: '基础' }, + { id: 'sec-transport', label: '运输' }, + { id: 'sec-load', label: '装货' }, + { id: 'sec-delivery', label: '送货' }, + { id: 'sec-remark', label: '备注' } +] +const mpActiveAnchor = ref('sec-basic') + +function scrollMpAnchor(id: string) { + mpActiveAnchor.value = id + document.getElementById(id)?.scrollIntoView({ behavior: 'smooth', block: 'start' }) +} +``` + +给现有各 `
` 加上对应 `id`(page 与 dialog 共用 id 无害)。 + +- [ ] **Step 2: page footer 贴底样式** + +确保 `.mp-plan-form-footer`: + +- `position: sticky; bottom: 0;` +- `padding-bottom: env(safe-area-inset-bottom)` +- 按钮可横向滚动或换行,文案已与 Web 一致(取消/暂存/创建/创建并安排运输等)— **核对** `showDraftButton` 等计算,禁止再引入 Demo 曾误加的「校验」按钮 + +- [ ] **Step 3: 窄屏走通新增四按钮** + +`/#/m/pl/plan/new`:底栏 4 键;点锚点可跳;安排模式 `/arrange` 仅取消+调整运输。 + +- [ ] **Step 4: Commit** + +```bash +git add web-admin/src/components/business/pl-plan-form-dialog/index.vue web-admin/src/views/m/pl/plan/form.vue +git commit -m "$(cat <<'EOF' +feat(web-admin): add mp plan form anchors and sticky footer. + +Improve the page-variant plan form for WebView without changing submit rules. +EOF +)" +``` + +--- + +### Task 7: 打印页与三方人车页打磨 + +**Files:** +- Modify: `web-admin/src/views/m/pl/plan/print.vue` +- Modify: `web-admin/src/views/m/pl/plan/paste.vue`(仅当 page 变体 footer/安全区不够时) +- 对照: Demo 打印 Chip + 送货客户;`PlPasteRecognizeDialog` 字段已全量则少改逻辑 + +- [ ] **Step 1: 打印 — 送货客户选择体验** + +现有 `el-select` 可保留;若窄屏难用,改为按钮打开 `MpBottomSheet` 列出 `customers`,选中后 `reloadPreview`。 +规则保持:`kind === 'delivery'`(及 blank-delivery 若支持)未选客户时提示,不生成预览。 + +确认 Chip 含空白入库/送货/分装(与 `MpPlanPrintKind` / Demo 一致)。 + +- [ ] **Step 2: 三方人车 — 验收 checklist** + +打开 `/#/m/pl/plan/paste`:识别 → 承运商/主挂/核载/驾押三要素/冲突 → 确认生成 → 回列表。若 footer 被遮,给 `PlPasteRecognizeDialog` page 变体补与表单相同的 sticky footer padding(最小改动)。 + +- [ ] **Step 3: Commit** + +```bash +git add web-admin/src/views/m/pl/plan/print.vue web-admin/src/views/m/pl/plan/paste.vue +# 若改了 paste dialog page 样式也一并 add +git commit -m "$(cat <<'EOF' +feat(web-admin): polish mp plan print customer pick and paste layout. + +Align WebView print/paste screens with the approved mobile demo interactions. +EOF +)" +``` + +--- + +### Task 8: 端到端冒烟清单(无新代码则只记录) + +**Files:** 无强制代码;若发现小缺口可热修后单独 commit。 + +- [ ] **Step 1: 按清单在窄屏/真机 WebView 勾选** + +| # | 路径 | 期望 | +|---|------|------| +| 1 | 列表双下拉 + 自定义日期 | 筛选生效 | +| 2 | Speed Dial 新增 / 三方 | 跳转正确 | +| 3 | 卡片更多 等通知/作废 | 与桌面同账号行为一致 | +| 4 | 详情分区 + 危化槽位 | 毒/爆槽位正确;未上传可见 | +| 5 | 详情底栏 | ≤2 主按钮 + 更多 | +| 6 | 新增四按钮 | 暂存/创建/创建并安排 | +| 7 | 安排危化未齐 | 拦截提示 | +| 8 | 打印送货单 | 必选客户 | +| 9 | 三方人车 | 全字段可提交 | +| 10 | 现场原生「运输计划单」 | 不受影响 | + +- [ ] **Step 2: 若有热修,单独 commit;否则在 PR 描述粘贴冒烟结果** + +--- + +## Spec 覆盖自检 + +| Spec 项 | Task | +|---------|------| +| D7 双下拉 + 自定义日期 + PL_PLAN_LIST_TABS | Task 4 | +| D8 Speed Dial | Task 4 | +| D9 详情底栏 2 主 + 更多 | Task 3、5 | +| D10 危化证详情槽位 | Task 5 | +| D11 运费只读 | Task 5 | +| D12 打印选客户 | Task 7 | +| D3/D4/D5 表单锚点、同表单安排、Sheet | Task 1、6(选择器以 page 变体渐进;列表/详情更多已用 Sheet) | +| D6 不改壳/ticket | 全文文件结构 | +| 三方人车全字段 | Task 7 | +| 非目标:运费编辑/证上传页 | 无对应 Task | + +**说明:** 表单内全部桌面 `el-select` 改为 Sheet 工作量大;本期以锚点+贴底+可用触控高度为必达,车辆库/承运商库等已有移动入口按钮的保持;若验收仍认为选择器过重,再开增量 Task 把高频选择器换成 `MpBottomSheet`,不阻塞本计划合入。 + +--- + +## 执行交接 + +Plan 完成后请选择执行方式(见下一条用户提示)。