Files
shanxia2.0/docs/superpowers/plans/2026-09-15-mp-webview-pl-plan-mobile-redesign.md
T
wuzilongandCursor 3795fa2b5a docs: add implementation plan for mp plan mobile redesign.
Break the approved WebView UI redesign into sequenced tasks for list, detail, form, print, and paste.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-15 01:46:49 +08:00

25 KiB
Raw Blame History

小程序 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,勿改坏已有页):

/* === 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
<!-- web-admin/src/components/mp/MpBottomSheet.vue -->
<template>
  <Teleport to="body">
    <div v-show="modelValue" class="mp-sheet-mask" @click="close" />
    <div
      v-show="modelValue"
      class="mp-sheet"
      :class="{ 'mp-sheet--half': half }"
      role="dialog"
      aria-modal="true"
    >
      <div class="mp-sheet-handle" />
      <div class="mp-sheet-head">
        <h3>{{ title }}</h3>
        <button class="mp-sheet-close" type="button" @click="close">关闭</button>
      </div>
      <div v-if="searchable" class="mp-sheet-search">
        <input
          v-model="keyword"
          class="mp-sheet-search-input"
          type="search"
          :placeholder="searchPlaceholder"
          autocomplete="off"
        />
      </div>
      <div class="mp-sheet-body">
        <slot :keyword="keyword" />
      </div>
      <div v-if="$slots.footer" class="mp-sheet-foot">
        <slot name="footer" />
      </div>
    </div>
  </Teleport>
</template>

<script setup lang="ts">
  import { ref, watch } from 'vue'

  const props = withDefaults(
    defineProps<{
      modelValue: boolean
      title?: string
      searchable?: boolean
      searchPlaceholder?: string
      half?: boolean
    }>(),
    {
      title: '',
      searchable: false,
      searchPlaceholder: '搜索',
      half: false
    }
  )
  const emit = defineEmits<{ 'update:modelValue': [boolean] }>()
  const keyword = ref('')

  watch(
    () => props.modelValue,
    (open) => {
      if (open) keyword.value = ''
    }
  )

  function close() {
    emit('update:modelValue', false)
  }
</script>

<style scoped lang="scss">
  .mp-sheet-mask {
    position: fixed;
    inset: 0;
    z-index: 2000;
    background: rgba(0, 0, 0, 0.4);
    backdrop-filter: none;
  }
  .mp-sheet {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 2001;
    max-height: 78vh;
    display: flex;
    flex-direction: column;
    background: #fff;
    border-radius: 16px 16px 0 0;
    padding-bottom: env(safe-area-inset-bottom, 0px);
  }
  .mp-sheet--half { max-height: 50vh; }
  .mp-sheet-handle {
    width: 36px;
    height: 4px;
    margin: 8px auto 0;
    border-radius: 999px;
    background: #dcebe4;
  }
  .mp-sheet-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 10px 16px;
  }
  .mp-sheet-head h3 { margin: 0; font-size: 16px; }
  .mp-sheet-close {
    border: none;
    background: none;
    color: #0a9f6d;
    font-size: 14px;
    cursor: pointer;
  }
  .mp-sheet-search { padding: 0 16px 8px; }
  .mp-sheet-search-input {
    width: 100%;
    height: 36px;
    padding: 0 12px;
    border: 1px solid #dcebe4;
    border-radius: 8px;
    font-size: 14px;
  }
  .mp-sheet-body {
    flex: 1;
    overflow: auto;
    padding: 0 8px 12px;
    -webkit-overflow-scrolling: touch;
  }
  .mp-sheet-foot {
    display: flex;
    gap: 8px;
    padding: 10px 16px;
    border-top: 1px solid #dcebe4;
  }
</style>
  • Step 3: 浏览器确认样式文件无 SCSS 语法错误
cd web-admin && pnpm exec vite build --mode development 2>&1 | tail -30

若全量 build 过慢,至少 pnpm dev 启动后打开任意 /#/m/pl/plan 看控制台无 SCSS 报错。

  • Step 4: Commit
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 返回字段

// 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: 写单测(字段存在)
// 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:

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: 跑测
cd web-admin && pnpm exec vitest run src/utils/mp-pl-plan-card.spec.ts

Expected: PASS(或按项目实际测试命令)。

  • Step 4: Commit
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: 实现拆分逻辑

// 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: 单测
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
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 处理。

卡片更多操作需:

import {
  canArrangeTransport,
  arrangeTransportLabel,
  canWaitNotice,
  canCancelWaitNotice,
  canVoidPlan
} from '@/utils/pl-plan-action-guards'

作废/等通知调用与详情页相同的 planApi 方法(从现有 detail.vue 复制调用方式,勿发明新 API)。

  • Step 2: 手动验收列表
cd web-admin && pnpm dev

浏览器开 /#/m/pl/plan(需 ticket 或本地已登录 mp 调试方式,与现网一致)。核对:

  • Tab 顺序含「全部」且在草稿后

  • 自定义日期可筛

  • Speed Dial 两项可跳

  • 卡片有供方→需方、跟进人

  • Step 3: Commit

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. 危化:<PlPlanHazardCertViewTable :products="hazardProducts" /> + 齐备 Tag(isHazardCertReady 若 API 有字段)
  5. 运费:只读 kv(是否有运费、公里、方式、量、单价、金额、开票、备注)— 无运费时其余为 —
  6. 备注

底栏:splitMpPlanDetailActions → ⋯ 打开 MpBottomSheet;主按钮绑定 edit/arrange/copy/wait/void/print 导航与现有 API。

导航:

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
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 下增加:

<nav v-if="isPageVariant" class="mp-plan-anchor-bar">
  <button
    v-for="item in mpAnchors"
    :key="item.id"
    type="button"
    class="mp-plan-anchor"
    :class="{ active: mpActiveAnchor === item.id }"
    @click="scrollMpAnchor(item.id)"
  >
    {{ item.label }}
  </button>
</nav>
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' })
}

给现有各 <section class="pl-plan-form-section"> 加上对应 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
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
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 完成后请选择执行方式(见下一条用户提示)。