Просмотр исходного кода

docs: add mobile redesign spec for WebView transport plans.

Capture the approved HTML demo as the UI authority for rewriting /#/m/pl/* without changing ticket or shell architecture.

Co-authored-by: Cursor <cursoragent@cursor.com>
wuzilong 2 месяцев назад
Родитель
Сommit
43f5ae8603

+ 2589 - 0
bak/mobile-ui/shanxia-mp-pl-plan-mobile-redesign-demo.html

@@ -0,0 +1,2589 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+  <meta charset="UTF-8" />
+  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
+  <title>山峡化工 · 运输计划移动端重设计 Demo</title>
+  <style>
+    :root {
+      --brand: #0a9f6d;
+      --brand-dark: #087f57;
+      --brand-soft: #e9f9f2;
+      --barley: #d6a343;
+      --barley-soft: #fff7e8;
+      --bg: #f3f6f5;
+      --surface: #fff;
+      --text: #1f2a26;
+      --muted: #6b7b74;
+      --border: #dcebe4;
+      --danger: #d14343;
+      --warning: #cf8a19;
+      --radius: 14px;
+      --shadow: 0 6px 16px rgba(10, 159, 109, 0.08);
+      --header: linear-gradient(145deg, #087f57 0%, #0a9f6d 72%, #d6a343 165%);
+      --safe-b: env(safe-area-inset-bottom, 0px);
+    }
+    * { box-sizing: border-box; margin: 0; padding: 0; }
+    button, input, select, textarea { font: inherit; }
+    body {
+      background: #cfd9d4;
+      color: var(--text);
+      font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Helvetica Neue', sans-serif;
+      font-size: 14px;
+      line-height: 1.45;
+      min-height: 100vh;
+    }
+    .stage { max-width: 440px; margin: 0 auto; padding: 16px 12px 32px; }
+    .banner {
+      background: #1f2a26; color: #fff; border-radius: 12px; padding: 12px 14px;
+      margin-bottom: 12px; font-size: 12px; line-height: 1.55;
+    }
+    .banner strong { color: #9fe3c4; }
+    .banner code { background: rgba(255,255,255,.12); padding: 1px 5px; border-radius: 4px; }
+    .phone {
+      position: relative;
+      background: var(--bg);
+      border-radius: 28px;
+      overflow: hidden;
+      border: 8px solid #24312c;
+      height: 780px;
+      box-shadow: 0 18px 40px rgba(0,0,0,.16);
+      display: flex;
+      flex-direction: column;
+    }
+    .status-bar {
+      flex: 0 0 auto;
+      height: 28px; background: var(--brand-dark); color: rgba(255,255,255,.85);
+      display: flex; align-items: center; justify-content: space-between;
+      padding: 0 16px; font-size: 11px;
+    }
+    .screen-host { flex: 1; position: relative; overflow: hidden; }
+    .screen {
+      position: absolute; inset: 0; display: none; flex-direction: column;
+      background: var(--bg); overflow: hidden;
+    }
+    .screen.active { display: flex; }
+    .header {
+      flex: 0 0 auto;
+      background: var(--header); color: #fff; padding: 8px 14px 14px;
+    }
+    .header-row { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
+    .back {
+      border: 0; background: transparent; color: #fff; opacity: .95;
+      font-size: 13px; cursor: pointer; padding: 4px 0;
+    }
+    .header h1 { font-size: 20px; font-weight: 700; margin-top: 6px; }
+    .header .sub { margin-top: 2px; font-size: 12px; opacity: .88; }
+    .search-wrap { margin-top: 10px; }
+    .search {
+      width: 100%; border: 0; border-radius: 999px; padding: 10px 14px;
+      background: rgba(255,255,255,.96); color: var(--text);
+    }
+    .search::placeholder { color: #9aa8a1; }
+    .scroll { flex: 1; min-height: 0; overflow-y: auto; -webkit-overflow-scrolling: touch; }
+    .scroll.pad-fab { padding-bottom: 12px; }
+    .scroll.pad-speed { padding-bottom: 100px; }
+    .chips, .tabs, .toolbar {
+      display: flex; gap: 8px; overflow-x: auto; padding: 10px 12px 0;
+      -webkit-overflow-scrolling: touch;
+    }
+    .tabs { padding-top: 8px; }
+    .toolbar { padding: 8px 12px 10px; }
+    .chip, .tab {
+      flex: 0 0 auto; border-radius: 999px; border: 1px solid var(--border);
+      background: #fff; color: var(--muted); font-size: 12px; padding: 6px 12px; cursor: pointer;
+    }
+    .chip.active, .tab.active {
+      background: var(--brand-soft); border-color: #b7ebce; color: var(--brand-dark); font-weight: 600;
+    }
+    .tab.active { background: var(--brand); border-color: var(--brand); color: #fff; }
+    /* JD 风格:检索下双下拉 */
+    .filter-bar {
+      flex: 0 0 auto;
+      display: grid;
+      grid-template-columns: 1fr 1fr;
+      background: #fff;
+      border-bottom: 1px solid var(--border);
+      position: relative;
+      z-index: 8;
+    }
+    .filter-trig {
+      display: flex; align-items: center; justify-content: center; gap: 4px;
+      height: 42px; border: 0; background: transparent; color: var(--text);
+      font-size: 13px; cursor: pointer; position: relative;
+    }
+    .filter-trig + .filter-trig::before {
+      content: ''; position: absolute; left: 0; top: 12px; bottom: 12px;
+      width: 1px; background: var(--border);
+    }
+    .filter-trig .arrow {
+      font-size: 10px; color: var(--muted); transition: transform .15s;
+    }
+    .filter-trig.open { color: var(--brand-dark); font-weight: 600; }
+    .filter-trig.open .arrow { color: var(--brand); transform: rotate(180deg); }
+    .filter-trig .val { max-width: 7em; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+    .filter-panel {
+      display: none; position: absolute; left: 0; right: 0; top: 100%;
+      background: #fff; border-bottom: 1px solid var(--border);
+      box-shadow: 0 10px 20px rgba(0,0,0,.08); max-height: 360px; overflow-y: auto; z-index: 9;
+    }
+    .filter-panel.open { display: block; }
+    .filter-opt {
+      display: flex; align-items: center; justify-content: space-between;
+      width: 100%; border: 0; background: #fff; text-align: left;
+      padding: 12px 16px; font-size: 14px; color: var(--text); cursor: pointer;
+      border-bottom: 1px solid #f0f4f2;
+    }
+    .filter-opt:active { background: var(--brand-soft); }
+    .filter-opt.active { color: var(--brand-dark); font-weight: 600; }
+    .filter-opt .check { color: var(--brand); font-size: 14px; visibility: hidden; }
+    .filter-opt.active .check { visibility: visible; }
+    .filter-mask {
+      display: none; position: absolute; left: 0; right: 0; top: 0; bottom: 0;
+      background: rgba(15, 30, 24, .35); z-index: 7;
+    }
+    .filter-mask.open { display: block; }
+    /* 右下角圆形悬浮 + 展开菜单 */
+    .speed-dial {
+      position: absolute; right: 16px; bottom: calc(16px + var(--safe-b));
+      z-index: 12; display: flex; flex-direction: column; align-items: flex-end; gap: 10px;
+    }
+    .speed-dial-menu {
+      display: none; flex-direction: column; align-items: flex-end; gap: 10px;
+    }
+    .speed-dial.open .speed-dial-menu { display: flex; }
+    .speed-item {
+      display: flex; align-items: center; gap: 8px; border: 0; background: transparent; cursor: pointer;
+      padding: 0;
+    }
+    .speed-item .label {
+      background: rgba(31,42,38,.88); color: #fff; font-size: 12px;
+      padding: 6px 10px; border-radius: 999px; white-space: nowrap;
+      box-shadow: 0 4px 12px rgba(0,0,0,.12);
+    }
+    .speed-item .mini {
+      width: 44px; height: 44px; border-radius: 50%; border: 0;
+      background: #fff; color: var(--brand-dark); font-size: 12px; font-weight: 700;
+      box-shadow: 0 6px 16px rgba(10,159,109,.2); display: flex; align-items: center; justify-content: center;
+      pointer-events: none;
+    }
+    .speed-item.primary .mini { background: var(--brand); color: #fff; }
+    .speed-main {
+      width: 56px; height: 56px; border-radius: 50%; border: 0;
+      background: var(--brand); color: #fff; font-size: 28px; line-height: 1;
+      font-weight: 400; cursor: pointer;
+      box-shadow: 0 8px 20px rgba(10,159,109,.35);
+      display: flex; align-items: center; justify-content: center;
+      transition: transform .18s ease, background .18s;
+    }
+    .speed-dial.open .speed-main {
+      background: var(--brand-dark); transform: rotate(45deg);
+    }
+    .speed-mask {
+      display: none; position: absolute; inset: 0; z-index: 11;
+      background: rgba(15, 30, 24, .28);
+    }
+    .speed-mask.open { display: block; }
+    .btn {
+      display: inline-flex; align-items: center; justify-content: center; gap: 4px;
+      height: 36px; padding: 0 12px; border-radius: 8px; border: 1px solid var(--border);
+      background: #fff; color: var(--text); font-size: 13px; cursor: pointer; white-space: nowrap;
+    }
+    .btn:active { transform: scale(.98); }
+    .btn-primary { background: var(--brand); border-color: var(--brand); color: #fff; font-weight: 600; }
+    .btn-ghost { background: var(--brand-soft); border-color: #b7ebce; color: var(--brand-dark); }
+    .btn-danger { color: var(--danger); border-color: #f0c2c2; background: #fff7f7; }
+    .btn-block { width: 100%; }
+    .btn-lg { height: 44px; font-size: 14px; }
+    .list { padding: 0 12px 12px; display: flex; flex-direction: column; gap: 10px; }
+    .card {
+      background: var(--surface); border-radius: var(--radius); box-shadow: var(--shadow);
+      border: 1px solid rgba(220,235,228,.85); padding: 12px; cursor: pointer;
+    }
+    .card-head { display: flex; justify-content: space-between; gap: 8px; align-items: flex-start; }
+    .plan-no { font-size: 15px; font-weight: 700; color: var(--brand-dark); }
+    .muted { color: var(--muted); font-size: 12px; }
+    .tag {
+      flex-shrink: 0; font-size: 11px; padding: 3px 8px; border-radius: 999px;
+      font-weight: 600; color: #fff; background: #64748b;
+    }
+    .tag-warn { background: #d97706; }
+    .tag-ok { background: #059669; }
+    .tag-wait { background: #cf8a19; }
+    .tag-mute { background: #64748b; }
+    .meta { margin-top: 8px; display: grid; gap: 3px; font-size: 12px; color: var(--muted); }
+    .meta b { color: var(--text); font-weight: 500; }
+    .products {
+      margin-top: 8px; background: var(--brand-soft); border-radius: 10px;
+      padding: 8px 10px; font-size: 12px; color: var(--brand-dark);
+    }
+    .card-actions { margin-top: 10px; display: flex; justify-content: flex-end; gap: 8px; flex-wrap: wrap; }
+    .empty { padding: 40px 16px; text-align: center; color: var(--muted); font-size: 13px; }
+    .fab-bar {
+      position: absolute; left: 0; right: 0; bottom: 0;
+      padding: 10px 12px calc(12px + var(--safe-b));
+      background: linear-gradient(180deg, transparent, var(--bg) 26%);
+      display: flex; gap: 8px; z-index: 5;
+    }
+    /* 详情/表单/打印/三方:底栏参与 flex,贴死屏幕底,无悬空空隙 */
+    #screen-paste .fab-bar,
+    #screen-detail .fab-bar,
+    #screen-form .fab-bar,
+    #screen-print .fab-bar {
+      position: relative;
+      left: auto;
+      right: auto;
+      bottom: auto;
+      flex: 0 0 auto;
+      width: 100%;
+      margin: 0;
+      padding: 10px 12px;
+      background: #fff;
+      border-top: 1px solid var(--border);
+      box-shadow: 0 -4px 16px rgba(0,0,0,.04);
+      z-index: 6;
+    }
+    .fab-bar .btn { flex: 1; height: 44px; }
+    .fab-bar .btn-primary { flex: 1.4; }
+    #form-fab {
+      flex-wrap: wrap;
+    }
+    #form-fab .btn {
+      flex: 1 1 calc(50% - 4px);
+      min-width: calc(50% - 4px);
+      height: 40px;
+      font-size: 13px;
+    }
+    #form-fab .btn-primary {
+      flex: 1 1 100%;
+    }
+    #detail-fab {
+      align-items: center;
+    }
+    #detail-fab .btn {
+      flex: 1;
+      height: 44px;
+    }
+    #detail-fab .btn-more {
+      flex: 0 0 48px;
+      width: 48px;
+      padding: 0;
+      font-size: 22px;
+      line-height: 1;
+      letter-spacing: 1px;
+      color: var(--text);
+      border-color: var(--border);
+      background: #fff;
+    }
+    #detail-fab .btn-primary {
+      flex: 1.2;
+    }
+    .more-action {
+      display: flex;
+      align-items: center;
+      justify-content: space-between;
+      width: 100%;
+      border: 0;
+      background: #fff;
+      text-align: left;
+      padding: 14px 12px;
+      font-size: 15px;
+      color: var(--text);
+      cursor: pointer;
+      border-bottom: 1px solid #eef4f1;
+    }
+    .more-action:active { background: var(--brand-soft); }
+    .more-action.danger { color: var(--danger); }
+    .link-row {
+      display: flex; gap: 8px; align-items: center;
+    }
+    .link-row .control { flex: 1; min-width: 0; font-size: 12px; }
+    .link-row .btn { flex: 0 0 auto; height: 40px; padding: 0 10px; }
+    .detail-remark {
+      font-size: 13px; color: var(--text); line-height: 1.55;
+      white-space: pre-wrap; word-break: break-word;
+    }
+    .list-more-wrap { padding: 4px 12px 20px; }
+    #custom-date-row.hidden { display: none !important; }
+    .prod-table {
+      margin-top: 8px; border: 1px solid var(--border); border-radius: 10px; overflow: hidden;
+      background: #fff;
+    }
+    .prod-row {
+      padding: 10px; border-bottom: 1px solid #eef4f1; font-size: 12px;
+    }
+    .prod-row:last-child { border-bottom: 0; }
+    .prod-row__name { font-size: 13px; font-weight: 700; color: var(--brand-dark); margin-bottom: 6px; }
+    .prod-row__grid {
+      display: grid; grid-template-columns: 1fr 1fr; gap: 4px 10px; color: var(--muted);
+    }
+    .prod-row__grid b { color: var(--text); font-weight: 500; }
+    .delivery-tabs {
+      display: flex; gap: 6px; overflow-x: auto; margin: 8px 0 4px;
+      -webkit-overflow-scrolling: touch;
+    }
+    .delivery-tab {
+      flex: 0 0 auto; border: 1px solid var(--border); background: #fff;
+      border-radius: 999px; padding: 6px 12px; font-size: 12px; color: var(--muted); cursor: pointer;
+    }
+    .delivery-tab.active {
+      background: var(--brand); border-color: var(--brand); color: #fff; font-weight: 600;
+    }
+    .feedback-box {
+      margin-top: 8px; padding: 10px; border-radius: 10px; background: #f7faf8;
+      border: 1px dashed var(--border); font-size: 12px;
+    }
+    .feedback-box h4 { font-size: 12px; color: var(--brand-dark); margin-bottom: 6px; }
+    .site-link-list { display: grid; gap: 8px; }
+    .vehicle-view-btn { margin-top: 8px; }
+    .cert-summary {
+      display: flex; align-items: center; justify-content: space-between; gap: 8px;
+      margin-bottom: 10px; font-size: 12px; color: var(--muted);
+    }
+    .cert-summary .tag { flex-shrink: 0; }
+    .cert-list { display: grid; gap: 10px; }
+    .cert-block {
+      border: 1px solid var(--border); border-radius: 10px; background: #fff; padding: 10px;
+    }
+    .cert-block__head {
+      display: flex; align-items: center; flex-wrap: wrap; gap: 6px; margin-bottom: 8px;
+    }
+    .cert-block__index { font-size: 12px; color: var(--muted); }
+    .cert-block__name { font-size: 14px; font-weight: 700; color: var(--brand-dark); }
+    .cert-mark {
+      display: inline-flex; align-items: center; justify-content: center;
+      min-width: 22px; height: 20px; padding: 0 6px; border-radius: 4px;
+      font-size: 11px; font-weight: 700; color: #fff;
+    }
+    .cert-mark.toxic { background: #d97706; }
+    .cert-mark.explosive { background: #dc2626; }
+    .cert-slots { display: grid; gap: 8px; }
+    .cert-slot {
+      display: flex; align-items: flex-start; justify-content: space-between; gap: 8px;
+      padding: 8px; border-radius: 8px; background: #f7faf8;
+    }
+    .cert-slot__label { font-size: 12px; color: var(--text); font-weight: 600; flex-shrink: 0; padding-top: 2px; }
+    .cert-slot__body { flex: 1; min-width: 0; text-align: right; }
+    .cert-file {
+      display: inline-flex; align-items: center; gap: 4px; max-width: 100%;
+      padding: 4px 8px; border-radius: 6px; border: 1px solid var(--border);
+      background: #fff; font-size: 12px; color: var(--brand-dark); cursor: pointer;
+    }
+    .cert-file span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+    .cert-missing { font-size: 12px; color: #dc2626; font-weight: 600; }
+    .cert-hint { margin-top: 10px; font-size: 12px; color: var(--muted); line-height: 1.45; }
+    .section {
+      margin: 10px 12px; background: #fff; border-radius: var(--radius);
+      padding: 12px; box-shadow: var(--shadow);
+    }
+    .section h3 {
+      font-size: 13px; color: var(--brand-dark); margin-bottom: 8px;
+      display: flex; align-items: center; justify-content: space-between;
+    }
+    .kv {
+      display: flex; justify-content: space-between; gap: 12px;
+      padding: 7px 0; font-size: 13px; border-bottom: 1px dashed var(--border);
+    }
+    .kv:last-child { border-bottom: 0; }
+    .kv > span:first-child { color: var(--muted); flex-shrink: 0; }
+    .kv > span:last-child { text-align: right; }
+    .actions-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
+    .actions-grid .btn { height: 42px; }
+    .anchor-bar {
+      display: flex; gap: 6px; overflow-x: auto; padding: 8px 12px;
+      background: #fff; border-bottom: 1px solid var(--border); flex: 0 0 auto;
+    }
+    .anchor {
+      flex: 0 0 auto; border: 1px solid var(--border); background: #fff;
+      color: var(--muted); border-radius: 999px; padding: 5px 10px; font-size: 12px; cursor: pointer;
+    }
+    .anchor.active { background: var(--brand-soft); border-color: #b7ebce; color: var(--brand-dark); font-weight: 600; }
+    .field { margin-bottom: 12px; }
+    .field:last-child { margin-bottom: 0; }
+    .field label {
+      display: block; font-size: 12px; color: var(--muted); margin-bottom: 6px;
+    }
+    .field-row { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
+    .control, .picker {
+      width: 100%; border: 1px solid var(--border); border-radius: 8px;
+      background: #fff; padding: 10px 12px; color: var(--text); text-align: left;
+    }
+    .picker { cursor: pointer; display: flex; justify-content: space-between; align-items: center; gap: 8px; }
+    .picker .ph { color: #9aa8a1; }
+    .picker .chev { color: var(--muted); font-size: 12px; }
+    textarea.control { min-height: 72px; resize: vertical; }
+    .stop-card {
+      border: 1px solid var(--border); border-radius: 10px; padding: 10px; margin-top: 8px;
+      background: #fafcfb;
+    }
+    .stop-card + .stop-card { margin-top: 8px; }
+    .stop-head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; }
+    .stop-head b { font-size: 13px; }
+    .link-btn {
+      border: 0; background: transparent; color: var(--brand-dark);
+      font-size: 12px; cursor: pointer; font-weight: 600;
+    }
+    .link-btn.danger { color: var(--danger); }
+    .hint-box {
+      margin: 8px 12px 0; padding: 8px 10px; border-radius: 8px;
+      background: var(--barley-soft); color: #7a5a18; font-size: 12px;
+    }
+    .print-preview {
+      margin: 12px; background: #fff; border-radius: 8px; border: 1px solid var(--border);
+      min-height: 360px; padding: 16px; font-size: 12px; color: #333;
+    }
+    .print-preview h2 { text-align: center; font-size: 16px; margin-bottom: 12px; }
+    .print-preview .line { display: flex; justify-content: space-between; padding: 4px 0; border-bottom: 1px dashed #eee; }
+    .sheet-mask {
+      position: absolute; inset: 0; background: rgba(15, 30, 24, .45);
+      z-index: 20; display: none;
+    }
+    .sheet-mask.open { display: block; }
+    .sheet {
+      position: absolute; left: 0; right: 0; bottom: 0; z-index: 21;
+      background: #fff; border-radius: 18px 18px 0 0;
+      max-height: 78%; transform: translateY(110%);
+      transition: transform .22s ease, visibility 0s linear .22s;
+      display: flex; flex-direction: column;
+      padding-bottom: var(--safe-b);
+      visibility: hidden;
+      pointer-events: none;
+    }
+    .sheet.open {
+      transform: translateY(0);
+      visibility: visible;
+      pointer-events: auto;
+      transition: transform .22s ease, visibility 0s;
+    }
+    .sheet.half { max-height: 42%; }
+    .sheet-handle {
+      width: 36px; height: 4px; border-radius: 999px; background: #d5e0db;
+      margin: 10px auto 6px;
+    }
+    .sheet-head {
+      display: flex; align-items: center; justify-content: space-between;
+      padding: 4px 16px 10px; border-bottom: 1px solid var(--border);
+    }
+    .sheet-head h3 { font-size: 15px; }
+    .sheet-body { overflow-y: auto; padding: 8px 12px 16px; flex: 1; }
+    .sheet-search {
+      padding: 0 12px 10px;
+      border-bottom: 1px solid var(--border);
+      flex: 0 0 auto;
+    }
+    .sheet-search input {
+      width: 100%;
+      border: 1px solid var(--border);
+      border-radius: 999px;
+      padding: 10px 14px;
+      background: #f5f8f7;
+      color: var(--text);
+      font-size: 14px;
+    }
+    .sheet-search input::placeholder { color: #9aa8a1; }
+    .sheet-empty {
+      padding: 28px 12px;
+      text-align: center;
+      color: var(--muted);
+      font-size: 13px;
+    }
+    .sheet-foot {
+      display: flex; gap: 8px; padding: 10px 12px; border-top: 1px solid var(--border);
+    }
+    .sheet-foot .btn { flex: 1; height: 44px; }
+    .option-row {
+      display: flex; align-items: center; justify-content: space-between;
+      gap: 10px; padding: 12px 10px; border-bottom: 1px solid #eef4f1; cursor: pointer;
+    }
+    .option-row:active { background: var(--brand-soft); }
+    .option-row .t { font-size: 14px; font-weight: 600; }
+    .option-row .d { font-size: 12px; color: var(--muted); margin-top: 2px; }
+    .toast {
+      position: absolute; left: 50%; bottom: 96px; transform: translateX(-50%);
+      background: rgba(31,42,38,.92); color: #fff; padding: 10px 16px;
+      border-radius: 999px; font-size: 13px; z-index: 40; display: none;
+      white-space: nowrap; max-width: 90%;
+    }
+    .toast.show { display: block; }
+    .map-nav {
+      margin-top: 10px; font-size: 11px; color: var(--muted); text-align: center;
+    }
+    .hidden { display: none !important; }
+    .paste-conflict-opt.active {
+      background: var(--brand-soft); border-color: #b7ebce; color: var(--brand-dark); font-weight: 600;
+    }
+    .btn[disabled] { opacity: .45; pointer-events: none; }
+    .check-row {
+      display: flex; align-items: center; gap: 8px;
+      font-size: 13px; color: var(--text); padding: 8px 0 0;
+    }
+    .product-card {
+      border: 1px solid var(--border); border-radius: 10px; padding: 10px;
+      margin-top: 8px; background: #fafcfb;
+    }
+    .product-card__title { font-size: 14px; font-weight: 700; margin-bottom: 8px; color: var(--brand-dark); }
+    .qty-balance {
+      margin-top: 12px; padding: 10px 12px; border-radius: 10px;
+      background: var(--brand-soft); color: var(--brand-dark); font-size: 12px;
+      display: grid; gap: 4px;
+    }
+    .qty-balance .qty-ok { font-weight: 700; margin-top: 2px; }
+  </style>
+</head>
+<body>
+  <div class="stage">
+    <div class="banner">
+      <strong>运输计划 · 移动端重设计 Demo(方案 1)</strong><br />
+      长页分区 + 锚点 · 选择器/确认为 Bottom Sheet · 安排/编辑对齐 Web 同一表单。<br />
+      文件:<code>bak/mobile-ui/shanxia-mp-pl-plan-mobile-redesign-demo.html</code>
+    </div>
+
+    <div class="phone" id="phone">
+      <div class="status-bar"><span>21:29</span><span>WebView Demo</span></div>
+      <div class="screen-host">
+
+        <!-- LIST -->
+        <section class="screen active" id="screen-list" data-title="列表">
+          <header class="header">
+            <div class="header-row">
+              <button class="back" type="button" data-action="toast" data-msg="返回工作台(Demo)">← 工作台</button>
+            </div>
+            <h1>运输计划</h1>
+            <div class="sub">小程序 WebView · 三墙 Token</div>
+            <div class="search-wrap">
+              <input class="search" id="list-keyword" type="search" placeholder="计划编号 / 供方 / 需方 / 产品 / 车牌号" />
+            </div>
+          </header>
+          <div class="filter-bar" id="filter-bar">
+            <button class="filter-trig" type="button" id="trig-date" data-action="toggle-filter" data-filter="date">
+              <span class="val" id="label-date">今天</span>
+              <span class="arrow">▼</span>
+            </button>
+            <button class="filter-trig" type="button" id="trig-tab" data-action="toggle-filter" data-filter="tab">
+              <span class="val" id="label-tab">待安排</span>
+              <span class="arrow">▼</span>
+            </button>
+            <div class="filter-panel" id="panel-date">
+              <button class="filter-opt active" type="button" data-action="pick-date" data-date="today" data-label="今天">今天<span class="check">✓</span></button>
+              <button class="filter-opt" type="button" data-action="pick-date" data-date="tomorrow" data-label="明天">明天<span class="check">✓</span></button>
+              <button class="filter-opt" type="button" data-action="pick-date" data-date="week" data-label="本周">本周<span class="check">✓</span></button>
+              <button class="filter-opt" type="button" data-action="pick-date" data-date="month" data-label="本月">本月<span class="check">✓</span></button>
+              <button class="filter-opt" type="button" data-action="pick-date" data-date="custom" data-label="自定义">自定义<span class="check">✓</span></button>
+              <div id="custom-date-row" class="hidden" style="padding:8px 12px 12px">
+                <div class="field-row" style="margin-bottom:8px">
+                  <input class="control" type="date" id="custom-date-from" value="2026-09-01" />
+                  <input class="control" type="date" id="custom-date-to" value="2026-09-14" />
+                </div>
+                <button class="btn btn-primary btn-block" type="button" data-action="apply-custom-date">确定</button>
+              </div>
+            </div>
+            <div class="filter-panel" id="panel-tab">
+              <button class="filter-opt active" type="button" data-action="pick-tab" data-tab="pending_arrange" data-label="待安排">待安排<span class="check">✓</span></button>
+              <button class="filter-opt" type="button" data-action="pick-tab" data-tab="arranged" data-label="已安排">已安排<span class="check">✓</span></button>
+              <button class="filter-opt" type="button" data-action="pick-tab" data-tab="drafting" data-label="草稿中">草稿中<span class="check">✓</span></button>
+              <button class="filter-opt" type="button" data-action="pick-tab" data-tab="all" data-label="全部">全部<span class="check">✓</span></button>
+              <button class="filter-opt" type="button" data-action="pick-tab" data-tab="inbound" data-label="入库单">入库单<span class="check">✓</span></button>
+              <button class="filter-opt" type="button" data-action="pick-tab" data-tab="outbound" data-label="出库单">出库单<span class="check">✓</span></button>
+              <button class="filter-opt" type="button" data-action="pick-tab" data-tab="direct" data-label="直销单">直销单<span class="check">✓</span></button>
+            </div>
+          </div>
+          <div class="filter-mask" id="filter-mask" data-action="close-filter"></div>
+          <div class="scroll pad-speed">
+            <div class="list" id="plan-list"></div>
+            <div class="list-more-wrap">
+              <button class="btn btn-block" type="button" id="btn-load-more" data-action="load-more">加载更多</button>
+            </div>
+          </div>
+          <div class="speed-mask" id="speed-mask" data-action="close-speed"></div>
+          <div class="speed-dial" id="speed-dial">
+            <div class="speed-dial-menu">
+              <button class="speed-item" type="button" data-nav="paste" data-action="close-speed">
+                <span class="label">三方人车快速创建</span>
+                <span class="mini">人车</span>
+              </button>
+              <button class="speed-item primary" type="button" data-nav="form" data-mode="new" data-action="close-speed">
+                <span class="label">新增计划</span>
+                <span class="mini">新增</span>
+              </button>
+            </div>
+            <button class="speed-main" type="button" data-action="toggle-speed" aria-label="快捷操作">+</button>
+          </div>
+        </section>
+
+        <!-- DETAIL -->
+        <section class="screen" id="screen-detail" data-title="详情">
+          <header class="header">
+            <div class="header-row">
+              <button class="back" type="button" data-nav="list">← 列表</button>
+              <span class="tag" id="detail-tag">待安排</span>
+            </div>
+            <h1 id="detail-no">JH20260914003</h1>
+            <div class="sub" id="detail-sub">出库单 · 计划日期 2026-09-14</div>
+          </header>
+          <div class="scroll pad-fab" id="detail-scroll">
+            <div class="section" id="d-sec-basic">
+              <h3>基础信息</h3>
+              <div class="kv"><span>计划编号</span><span id="d-plan-no">—</span></div>
+              <div class="kv"><span>计划日期</span><span id="d-plan-date">—</span></div>
+              <div class="kv"><span>归属公司</span><span id="d-company">安庆山峡</span></div>
+              <div class="kv"><span>单据类型</span><span id="d-type">—</span></div>
+              <div class="kv"><span>跟进人</span><span id="d-follow">王调度</span></div>
+            </div>
+
+            <div class="section" id="d-sec-transport">
+              <h3>运输信息</h3>
+              <div id="d-arrange-links" class="hidden">
+                <div class="field">
+                  <label>司机链接</label>
+                  <div class="link-row">
+                    <input class="control" id="d-driver-url" readonly value="https://w.example.com/h5/driver?t=***" />
+                    <button class="btn" type="button" data-action="toast" data-msg="已复制司机链接">复制</button>
+                  </div>
+                </div>
+                <div class="field">
+                  <label>现场链接</label>
+                  <div class="site-link-list" id="d-site-links"></div>
+                </div>
+              </div>
+              <div id="d-vehicle-block">
+                <div class="products" style="margin:0">
+                  <div><b id="d-v-plate">—</b></div>
+                  <div class="muted" style="margin-top:4px" id="d-v-trailer">挂车 —</div>
+                  <div class="muted" style="margin-top:2px" id="d-v-people">司机 — · 押运 —</div>
+                  <div class="muted" style="margin-top:2px" id="d-v-carrier">承运商 —</div>
+                  <button class="btn btn-block vehicle-view-btn" type="button" id="d-v-view" data-action="open-vehicle-view">查看车辆详情</button>
+                </div>
+              </div>
+              <p id="d-vehicle-empty" class="muted hidden" style="padding:8px 0">暂无车辆信息</p>
+              <div id="d-feedback" class="hidden">
+                <div class="feedback-box">
+                  <h4>司机回传摘要</h4>
+                  <div id="d-fb-driver">装货已签到 · 预计 16:30 到场</div>
+                </div>
+                <div class="feedback-box">
+                  <h4>现场 / 地磅回传</h4>
+                  <div id="d-fb-site">现场已确认到场 · 地磅入库单已生成</div>
+                </div>
+              </div>
+            </div>
+
+            <div class="section" id="d-sec-stops">
+              <h3>装送货信息</h3>
+              <div class="stop-card" style="margin-top:0">
+                <div class="stop-head"><b>装货信息</b></div>
+                <div class="kv"><span>装货客户</span><span id="d-load-customer">安庆山峡仓储</span></div>
+                <div class="kv"><span>装货时间</span><span id="d-load-time">2026-09-14</span></div>
+                <div class="kv"><span>装货地址</span><span id="d-load-addr">安庆市迎江区化工园 A3</span></div>
+                <div class="kv"><span>联系方式</span><span id="d-load-contact">0556-1234567</span></div>
+                <div class="kv"><span>注意事项</span><span id="d-load-notes">进门登记</span></div>
+                <div class="kv"><span>装货合同</span><span id="d-load-contract">内部客户(无合同)</span></div>
+                <div class="kv"><span>地磅单</span><span id="d-load-dibang">生成(入库)</span></div>
+                <div class="prod-table" id="d-load-products"></div>
+              </div>
+              <div class="stop-card">
+                <div class="stop-head"><b>送货信息</b></div>
+                <div class="delivery-tabs" id="d-delivery-tabs"></div>
+                <div id="d-delivery-body">
+                  <div class="kv"><span>送货客户</span><span id="d-del-customer">—</span></div>
+                  <div class="kv"><span>送货时间</span><span id="d-del-time">—</span></div>
+                  <div class="kv"><span>送货地址</span><span id="d-del-addr">—</span></div>
+                  <div class="kv"><span>联系方式</span><span id="d-del-contact">—</span></div>
+                  <div class="kv"><span>注意事项</span><span id="d-del-notes">—</span></div>
+                  <div class="kv"><span>送货合同</span><span id="d-del-contract">—</span></div>
+                  <div class="kv"><span>地磅单</span><span id="d-del-dibang">—</span></div>
+                  <div class="prod-table" id="d-del-products"></div>
+                </div>
+              </div>
+              <div class="qty-balance">
+                <div>计划总量 <b id="d-qty-plan">32</b> 吨</div>
+                <div>装货合计 <b id="d-qty-load">32</b> 吨</div>
+                <div>送货合计 <b id="d-qty-del">32</b> 吨</div>
+                <div class="qty-ok" id="d-qty-balance">✓ 数量平衡</div>
+              </div>
+            </div>
+
+            <div class="section" id="d-sec-cert">
+              <h3>危化品证书信息</h3>
+              <div class="cert-summary" id="d-cert-summary">
+                <span id="d-cert-summary-text">对齐 Web:按产品展示所需证书槽位</span>
+                <span class="tag tag-mute" id="d-cert-ready-tag">—</span>
+              </div>
+              <div class="cert-list" id="d-cert-list"></div>
+              <p class="muted hidden" id="d-cert-empty" style="padding:4px 0">暂无易制毒/易制爆产品</p>
+              <p class="cert-hint" id="d-cert-hint">上传与齐备确认在「危化品证上传」页操作(本 Demo 仅展示)。</p>
+            </div>
+
+            <div class="section" id="d-sec-freight">
+              <h3>运费信息</h3>
+              <div class="kv"><span>是否有运输费用</span><span id="d-freight-has">是</span></div>
+              <div class="kv"><span>运输公里数</span><span id="d-freight-km">186</span></div>
+              <div class="kv"><span>运费计算方式</span><span id="d-freight-type">按重量</span></div>
+              <div class="kv"><span>运输量(吨)</span><span id="d-freight-qty">32</span></div>
+              <div class="kv"><span>运输单价(元/吨)</span><span id="d-freight-price">120.00</span></div>
+              <div class="kv"><span>运费金额(元)</span><span id="d-freight-amount">3,840.00</span></div>
+              <div class="kv"><span>是否开票</span><span id="d-freight-invoice">是</span></div>
+              <div class="kv"><span>运费备注</span><span id="d-freight-remark">含过路费</span></div>
+            </div>
+
+            <div class="section" id="d-sec-remark">
+              <h3>备注</h3>
+              <p class="detail-remark" id="d-remark">注意厂区进门登记</p>
+            </div>
+          </div>
+          <div class="fab-bar" id="detail-fab"></div>
+        </section>
+
+        <!-- FORM -->
+        <section class="screen" id="screen-form" data-title="表单">
+          <header class="header">
+            <div class="header-row">
+              <button class="back" type="button" data-action="back">← 返回</button>
+            </div>
+            <h1 id="form-title">新增运输计划</h1>
+            <div class="sub" id="form-sub">长页分区 · 锚点跳转 · 对齐 Web 同一表单</div>
+          </header>
+          <div class="anchor-bar" id="anchor-bar">
+            <button class="anchor active" type="button" data-anchor="sec-basic">基础</button>
+            <button class="anchor" type="button" data-anchor="sec-transport">运输</button>
+            <button class="anchor" type="button" data-anchor="sec-load">装货</button>
+            <button class="anchor" type="button" data-anchor="sec-delivery">送货</button>
+            <button class="anchor" type="button" data-anchor="sec-remark">备注</button>
+          </div>
+          <div class="scroll pad-fab" id="form-scroll">
+            <div class="hint-box" id="form-hint">对齐 Web 新增:取消 / 暂存 / 创建 / 创建并安排运输</div>
+
+            <div class="section" id="sec-basic">
+              <h3>基础信息</h3>
+              <div class="field-row">
+                <div class="field">
+                  <label>创建日期</label>
+                  <input class="control" type="date" value="2026-09-14" />
+                </div>
+                <div class="field">
+                  <label>计划日期</label>
+                  <input class="control" type="date" id="f-plan-date" value="2026-09-14" />
+                </div>
+              </div>
+              <div class="field-row">
+                <div class="field">
+                  <label>归属公司</label>
+                  <select class="control" id="f-company">
+                    <option>安庆山峡</option>
+                    <option>安徽云栋</option>
+                  </select>
+                </div>
+                <div class="field">
+                  <label>单据类型</label>
+                  <select class="control" id="f-type">
+                    <option>出库单</option>
+                    <option>入库单</option>
+                    <option>直销单</option>
+                  </select>
+                </div>
+              </div>
+              <div class="field">
+                <label>跟进人</label>
+                <input class="control" value="王调度" readonly />
+              </div>
+            </div>
+
+            <div class="section" id="sec-transport">
+              <h3>运输信息</h3>
+              <div class="field">
+                <label>车牌号</label>
+                <button class="picker" type="button" data-sheet="vehicle">
+                  <span id="f-plate"><span class="ph">请选择车牌号</span></span>
+                  <span class="chev">选择</span>
+                </button>
+              </div>
+              <div class="actions-grid" style="margin-bottom:12px">
+                <button class="btn" type="button" data-sheet="vehicle">从车辆库选择</button>
+                <button class="btn" type="button" data-nav="paste">三方人车快速创建</button>
+              </div>
+              <div class="field">
+                <label>选择司机(可选)</label>
+                <button class="picker" type="button" data-sheet="personnel" data-role="司机">
+                  <span id="f-driver"><span class="ph">请选择司机</span></span>
+                  <span class="chev">选择</span>
+                </button>
+              </div>
+              <div class="field">
+                <label>选择押运员(可选)</label>
+                <button class="picker" type="button" data-sheet="personnel" data-role="押运">
+                  <span id="f-escort"><span class="ph">请选择押运员</span></span>
+                  <span class="chev">选择</span>
+                </button>
+              </div>
+              <div class="field">
+                <label>运输公司</label>
+                <button class="picker" type="button" data-sheet="carrier" data-target="f-carrier">
+                  <span id="f-carrier"><span class="ph">请选择运输公司</span></span>
+                  <span class="chev">选择</span>
+                </button>
+              </div>
+              <button class="btn btn-block" type="button" style="margin-top:8px" data-sheet="carrier" data-target="f-carrier">从承运商库选择</button>
+              <div class="products" id="f-vehicle-summary" style="margin-top:12px;display:none">
+                <div><b id="f-vs-plate">—</b></div>
+                <div class="muted" style="margin-top:4px" id="f-vs-people">司机 — · 押运 —</div>
+                <div class="muted" style="margin-top:2px" id="f-vs-carrier">承运商 —</div>
+              </div>
+            </div>
+
+            <div class="section" id="sec-load">
+              <h3>装货信息</h3>
+              <div class="field">
+                <label>装货客户 <span style="color:var(--danger)">*</span></label>
+                <button class="picker" type="button" data-sheet="customer" data-target="load-c1">
+                  <span id="load-c1">安庆山峡仓储</span>
+                  <span class="chev">选择</span>
+                </button>
+              </div>
+              <div class="field">
+                <label>装货合同</label>
+                <button class="picker" type="button" data-sheet="contract" data-target="load-ct1">
+                  <span id="load-ct1"><span class="ph">请选择合同</span></span>
+                  <span class="chev">选择</span>
+                </button>
+              </div>
+              <div class="field-row">
+                <div class="field">
+                  <label>装货时间 <span style="color:var(--danger)">*</span></label>
+                  <input class="control" type="date" value="2026-09-14" />
+                </div>
+                <div class="field">
+                  <label>联系方式</label>
+                  <input class="control" value="0556-1234567" placeholder="联系方式" />
+                </div>
+              </div>
+              <div class="field">
+                <label>装货地址</label>
+                <div class="link-row">
+                  <input class="control" id="load-addr-1" value="安庆市迎江区化工园 A3" placeholder="装货地址" />
+                  <button class="btn" type="button" data-sheet="address" data-target="load-addr-1">地址库</button>
+                </div>
+              </div>
+              <div class="field">
+                <label>注意事项</label>
+                <input class="control" placeholder="装货注意事项" value="进门登记" />
+              </div>
+              <div class="field">
+                <label>地磅单</label>
+                <label class="check-row"><input type="checkbox" checked /> 生成地磅单(入库)</label>
+                <div class="muted" style="margin-top:4px">本段客户为内部客户时默认勾选</div>
+              </div>
+              <div class="stop-head" style="margin-top:12px">
+                <b>装货产品</b>
+                <button class="link-btn" type="button" data-action="add-product" data-side="load">+ 添加产品</button>
+              </div>
+              <div id="load-products">
+                <div class="product-card">
+                  <div class="product-card__title">甲醇</div>
+                  <div class="field-row">
+                    <div class="field">
+                      <label>型号规格</label>
+                      <input class="control" value="优等品" placeholder="型号规格" />
+                    </div>
+                    <div class="field">
+                      <label>品牌</label>
+                      <input class="control" value="山峡" placeholder="品牌" />
+                    </div>
+                  </div>
+                  <div class="field-row">
+                    <div class="field">
+                      <label>别名</label>
+                      <input class="control" value="木精" placeholder="别名" />
+                    </div>
+                    <div class="field">
+                      <label>可安排量</label>
+                      <input class="control" value="32" readonly />
+                    </div>
+                  </div>
+                  <div class="field-row">
+                    <div class="field">
+                      <label>计划数量 <span style="color:var(--danger)">*</span></label>
+                      <input class="control" type="number" value="32" />
+                    </div>
+                    <div class="field">
+                      <label>单位 <span style="color:var(--danger)">*</span></label>
+                      <select class="control"><option>吨</option><option>千克</option></select>
+                    </div>
+                  </div>
+                  <div class="field">
+                    <label>包装规格 <span style="color:var(--danger)">*</span></label>
+                    <select class="control"><option>吨袋</option><option>槽罐</option><option>桶装</option></select>
+                  </div>
+                  <button class="link-btn danger" type="button" data-action="toast" data-msg="至少保留一条产品">删除</button>
+                </div>
+              </div>
+            </div>
+
+            <div class="section" id="sec-delivery">
+              <h3>
+                送货信息
+                <span>
+                  <button class="link-btn" type="button" data-action="add-stop" data-side="delivery">+ 添加送货段</button>
+                </span>
+              </h3>
+              <div id="delivery-stops">
+                <div class="stop-card" data-side="delivery" data-stop="1">
+                  <div class="stop-head">
+                    <b>送货段 1</b>
+                    <span>
+                      <button class="link-btn" type="button" data-action="toast" data-msg="已上移(模拟)">上移</button>
+                      <button class="link-btn" type="button" data-action="toast" data-msg="已下移(模拟)">下移</button>
+                      <button class="link-btn danger" type="button" data-action="toast" data-msg="至少保留一段">删除</button>
+                    </span>
+                  </div>
+                  <div class="field">
+                    <label>送货客户 <span style="color:var(--danger)">*</span></label>
+                    <button class="picker" type="button" data-sheet="customer" data-target="del-c1">
+                      <span id="del-c1">合肥某化</span>
+                      <span class="chev">选择</span>
+                    </button>
+                  </div>
+                  <div class="field">
+                    <label>送货合同</label>
+                    <button class="picker" type="button" data-sheet="contract" data-target="del-ct1">
+                      <span id="del-ct1">HT202609-001</span>
+                      <span class="chev">选择</span>
+                    </button>
+                  </div>
+                  <div class="field-row">
+                    <div class="field">
+                      <label>送货时间 <span style="color:var(--danger)">*</span></label>
+                      <input class="control" type="date" value="2026-09-14" />
+                    </div>
+                    <div class="field">
+                      <label>联系方式</label>
+                      <input class="control" value="0551-88886666" />
+                    </div>
+                  </div>
+                  <div class="field">
+                    <label>送货地址</label>
+                    <div class="link-row">
+                      <input class="control" id="del-addr-1" value="合肥经开区某某路 88 号" />
+                      <button class="btn" type="button" data-sheet="address" data-target="del-addr-1">地址库</button>
+                    </div>
+                  </div>
+                  <div class="field">
+                    <label>注意事项</label>
+                    <input class="control" placeholder="送货注意事项" value="需预约卸货" />
+                  </div>
+                  <div class="field">
+                    <label>地磅单</label>
+                    <label class="check-row"><input type="checkbox" /> 生成地磅单(出库)</label>
+                    <div class="muted" style="margin-top:4px">该车辆地磅单可选,请自行勾选</div>
+                  </div>
+                  <div class="stop-head" style="margin-top:8px">
+                    <b>送货产品</b>
+                    <span>
+                      <button class="link-btn" type="button" data-action="toast" data-msg="已从装货产品同步">从装货同步</button>
+                      <button class="link-btn" type="button" data-sheet="product" data-target="del-p1">+ 选择产品</button>
+                    </span>
+                  </div>
+                  <div class="product-card">
+                    <div class="product-card__title" id="del-p1">甲醇</div>
+                    <div class="field-row">
+                      <div class="field">
+                        <label>型号规格</label>
+                        <input class="control" value="优等品" />
+                      </div>
+                      <div class="field">
+                        <label>品牌</label>
+                        <input class="control" value="山峡" />
+                      </div>
+                    </div>
+                    <div class="field-row">
+                      <div class="field">
+                        <label>计划数量 <span style="color:var(--danger)">*</span></label>
+                        <input class="control" type="number" value="32" />
+                      </div>
+                      <div class="field">
+                        <label>单位 <span style="color:var(--danger)">*</span></label>
+                        <select class="control"><option>吨</option></select>
+                      </div>
+                    </div>
+                    <div class="field">
+                      <label>包装规格 <span style="color:var(--danger)">*</span></label>
+                      <select class="control"><option>吨袋</option><option>槽罐</option></select>
+                    </div>
+                    <div class="field">
+                      <label>送货方式 <span style="color:var(--danger)">*</span></label>
+                      <select class="control"><option>送货上门</option><option>客户自提</option></select>
+                    </div>
+                  </div>
+                </div>
+              </div>
+              <div class="qty-balance">
+                <div>计划总量 <b>32</b> 吨</div>
+                <div>装货合计 <b>32</b> 吨</div>
+                <div>送货合计 <b>32</b> 吨</div>
+                <div class="qty-ok">✓ 数量平衡</div>
+              </div>
+            </div>
+
+            <div class="section" id="sec-remark">
+              <h3>备注</h3>
+              <div class="field">
+                <label>备注</label>
+                <textarea class="control" id="f-remark" placeholder="请输入备注">注意厂区进门登记</textarea>
+              </div>
+            </div>
+            <div class="map-nav">滚动或点上方锚点切换分区 · 字段对齐 Web 弹窗</div>
+          </div>
+          <div class="fab-bar" id="form-fab"></div>
+        </section>
+
+        <!-- PRINT -->
+        <section class="screen" id="screen-print" data-title="打印">
+          <header class="header">
+            <div class="header-row">
+              <button class="back" type="button" data-nav="detail">← 详情</button>
+            </div>
+            <h1>打印单据</h1>
+            <div class="sub">长按预览区可存图(Demo 示意)</div>
+          </header>
+          <div class="chips" id="print-kinds">
+            <button class="chip active" type="button" data-kind="inbound">入库单</button>
+            <button class="chip" type="button" data-kind="delivery">送(提)货单</button>
+            <button class="chip" type="button" data-kind="subpack">分装单</button>
+            <button class="chip" type="button" data-kind="blank-inbound">空白入库单</button>
+            <button class="chip" type="button" data-kind="blank-delivery">空白送货单</button>
+            <button class="chip" type="button" data-kind="blank-subpack">空白分装单</button>
+          </div>
+          <div class="scroll pad-fab">
+            <div class="hint-box">WebView 不调用系统打印机,引导长按存图。</div>
+            <div class="section" id="print-customer-block" style="margin:0 12px 12px">
+              <h3>送货客户</h3>
+              <div class="field" style="margin:0">
+                <label>打印送(提)货单时需选择送货段客户</label>
+                <button class="picker" type="button" data-action="pick-print-customer-open">
+                  <span id="print-customer-label"><span class="ph">请选择送货客户</span></span>
+                  <span class="chev">选择</span>
+                </button>
+              </div>
+            </div>
+            <div class="print-preview" id="print-preview"></div>
+          </div>
+          <div class="fab-bar">
+            <button class="btn" type="button" data-nav="detail">返回详情</button>
+            <button class="btn btn-primary" type="button" data-action="toast" data-msg="已模拟保存预览图">保存预览</button>
+          </div>
+        </section>
+
+        <!-- PASTE:字段对齐 Web PlPasteRecognizeDialog · vehicle-bundle -->
+        <section class="screen" id="screen-paste" data-title="三方人车">
+          <header class="header">
+            <div class="header-row">
+              <button class="back" type="button" data-nav="list">← 列表</button>
+            </div>
+            <h1>三方人车快速创建</h1>
+            <div class="sub">对齐 Web 全量字段 · 识别后可编辑</div>
+          </header>
+          <div class="scroll pad-fab">
+            <div class="section">
+              <h3>客户原文</h3>
+              <div class="field">
+                <label>请按示例格式填写或粘贴后点击识别</label>
+                <textarea class="control" id="paste-text" style="min-height:150px" placeholder="请粘贴客户原文…">车牌:皖HD1234皖HG88挂
+核载:34 吨
+驾驶员:张伟
+身份证号:340102199001011234
+手机号:13800001111
+押运员:李强
+身份证号:340102199002021234
+电话:13900002222</textarea>
+              </div>
+              <button class="btn btn-primary btn-block btn-lg" type="button" data-action="paste-parse">识别</button>
+            </div>
+
+            <div id="paste-result-block" class="hidden">
+              <div class="hint-box" id="paste-warn" style="display:none"></div>
+
+              <div class="section">
+                <h3>车辆信息</h3>
+                <div class="field">
+                  <label>承运商</label>
+                  <button class="picker" type="button" data-sheet="carrier">
+                    <span id="paste-carrier"><span class="ph">请选择承运商</span></span>
+                    <span class="chev">选择</span>
+                  </button>
+                </div>
+                <div class="field">
+                  <label>主车号</label>
+                  <input class="control" id="paste-plate" maxlength="20" />
+                </div>
+                <div class="field">
+                  <label>挂车号(必填;无挂车可填「货拉拉」)</label>
+                  <input class="control" id="paste-trailer" maxlength="20" placeholder="必填" />
+                </div>
+                <div class="field">
+                  <label>核载(吨)</label>
+                  <input class="control" id="paste-load" type="number" min="0" step="0.01" />
+                </div>
+              </div>
+
+              <div class="section">
+                <h3>驾驶员</h3>
+                <div class="field">
+                  <label>驾驶员</label>
+                  <input class="control" id="paste-driver-name" maxlength="100" />
+                </div>
+                <div class="field">
+                  <label>驾驶员手机</label>
+                  <input class="control" id="paste-driver-phone" maxlength="32" />
+                </div>
+                <div class="field">
+                  <label>驾驶员身份证</label>
+                  <input class="control" id="paste-driver-id" maxlength="32" />
+                </div>
+              </div>
+
+              <div class="section">
+                <h3>押运员</h3>
+                <div class="field">
+                  <label>押运员</label>
+                  <input class="control" id="paste-escort-name" maxlength="100" />
+                </div>
+                <div class="field">
+                  <label>押运员电话</label>
+                  <input class="control" id="paste-escort-phone" maxlength="32" />
+                </div>
+                <div class="field">
+                  <label>押运员身份证</label>
+                  <input class="control" id="paste-escort-id" maxlength="32" />
+                </div>
+              </div>
+
+              <div class="section" id="paste-conflict-block">
+                <h3>冲突处理</h3>
+                <div class="stop-card">
+                  <div class="muted" style="margin-bottom:8px">主车号已存在:皖H·D1234(演示)</div>
+                  <div class="field">
+                    <label>处理方式</label>
+                    <div class="actions-grid">
+                      <button class="btn filter-opt-like paste-conflict-opt active" type="button" data-action="pick-conflict" data-val="reuse">复用</button>
+                      <button class="btn paste-conflict-opt" type="button" data-action="pick-conflict" data-val="overwrite">覆盖更新</button>
+                      <button class="btn paste-conflict-opt" type="button" data-action="pick-conflict" data-val="cancel" style="grid-column:1/-1">取消创建本条</button>
+                    </div>
+                  </div>
+                </div>
+              </div>
+            </div>
+          </div>
+          <div class="fab-bar" id="paste-fab">
+            <button class="btn" type="button" data-nav="list">取消</button>
+            <button class="btn btn-primary" type="button" id="paste-submit-btn" data-action="paste-submit" disabled>确认生成</button>
+          </div>
+        </section>
+
+        <!-- sheets -->
+        <div class="sheet-mask" id="sheet-mask"></div>
+
+        <div class="sheet" id="sheet-vehicle">
+          <div class="sheet-handle"></div>
+          <div class="sheet-head"><h3>选择车辆</h3><button class="link-btn" type="button" data-action="close-sheet">关闭</button></div>
+          <div class="sheet-search">
+            <input class="sheet-search-input" type="search" placeholder="搜索车牌 / 挂车号" autocomplete="off" />
+          </div>
+          <div class="sheet-body" id="vehicle-options"></div>
+        </div>
+
+        <div class="sheet" id="sheet-personnel">
+          <div class="sheet-handle"></div>
+          <div class="sheet-head"><h3 id="personnel-title">选择人员</h3><button class="link-btn" type="button" data-action="close-sheet">关闭</button></div>
+          <div class="sheet-search">
+            <input class="sheet-search-input" type="search" placeholder="搜索姓名 / 手机号" autocomplete="off" />
+          </div>
+          <div class="sheet-body" id="personnel-options"></div>
+        </div>
+
+        <div class="sheet" id="sheet-customer">
+          <div class="sheet-handle"></div>
+          <div class="sheet-head"><h3>选择客户</h3><button class="link-btn" type="button" data-action="close-sheet">关闭</button></div>
+          <div class="sheet-search">
+            <input class="sheet-search-input" type="search" placeholder="搜索客户名称 / 地址" autocomplete="off" />
+          </div>
+          <div class="sheet-body" id="customer-options"></div>
+        </div>
+
+        <div class="sheet" id="sheet-product">
+          <div class="sheet-handle"></div>
+          <div class="sheet-head"><h3>选择产品</h3><button class="link-btn" type="button" data-action="close-sheet">关闭</button></div>
+          <div class="sheet-search">
+            <input class="sheet-search-input" type="search" placeholder="搜索产品名称 / 规格" autocomplete="off" />
+          </div>
+          <div class="sheet-body" id="product-options"></div>
+        </div>
+
+        <div class="sheet" id="sheet-carrier">
+          <div class="sheet-handle"></div>
+          <div class="sheet-head"><h3>选择承运商</h3><button class="link-btn" type="button" data-action="close-sheet">关闭</button></div>
+          <div class="sheet-search">
+            <input class="sheet-search-input" type="search" placeholder="搜索承运商名称" autocomplete="off" />
+          </div>
+          <div class="sheet-body" id="carrier-options"></div>
+        </div>
+
+        <div class="sheet" id="sheet-contract">
+          <div class="sheet-handle"></div>
+          <div class="sheet-head"><h3>选择合同</h3><button class="link-btn" type="button" data-action="close-sheet">关闭</button></div>
+          <div class="sheet-search">
+            <input class="sheet-search-input" type="search" placeholder="搜索合同编号 / 名称" autocomplete="off" />
+          </div>
+          <div class="sheet-body" id="contract-options"></div>
+        </div>
+
+        <div class="sheet" id="sheet-address">
+          <div class="sheet-handle"></div>
+          <div class="sheet-head"><h3>从地址库选择</h3><button class="link-btn" type="button" data-action="close-sheet">关闭</button></div>
+          <div class="sheet-search">
+            <input class="sheet-search-input" type="search" placeholder="搜索地址 / 联系人" autocomplete="off" />
+          </div>
+          <div class="sheet-body" id="address-options"></div>
+        </div>
+
+        <div class="sheet" id="sheet-vehicle-view">
+          <div class="sheet-handle"></div>
+          <div class="sheet-head"><h3>车辆详情</h3><button class="link-btn" type="button" data-action="close-sheet">关闭</button></div>
+          <div class="sheet-body" id="vehicle-view-body" style="padding:8px 4px 20px"></div>
+        </div>
+
+        <div class="sheet half" id="sheet-detail-more">
+          <div class="sheet-handle"></div>
+          <div class="sheet-head"><h3>更多操作</h3><button class="link-btn" type="button" data-action="close-sheet">关闭</button></div>
+          <div class="sheet-body" id="detail-more-options"></div>
+        </div>
+
+        <div class="sheet half" id="sheet-list-more">
+          <div class="sheet-handle"></div>
+          <div class="sheet-head"><h3>卡片操作</h3><button class="link-btn" type="button" data-action="close-sheet">关闭</button></div>
+          <div class="sheet-body" id="list-more-options"></div>
+        </div>
+
+        <div class="sheet half" id="sheet-confirm">
+          <div class="sheet-handle"></div>
+          <div class="sheet-head"><h3 id="confirm-title">确认</h3><button class="link-btn" type="button" data-action="close-sheet">关闭</button></div>
+          <div class="sheet-body"><p id="confirm-msg" style="padding:8px 4px;color:var(--muted);font-size:13px"></p></div>
+          <div class="sheet-foot">
+            <button class="btn" type="button" data-action="close-sheet">取消</button>
+            <button class="btn btn-primary" type="button" id="confirm-ok">确定</button>
+          </div>
+        </div>
+
+        <div class="toast" id="toast"></div>
+      </div>
+    </div>
+  </div>
+
+  <script>
+    const methanolProd = { name: '甲醇', spec: '优等品', unit: '吨', pack: '吨袋', qtyPlan: 32, qtyActual: '—', method: '送货上门', alias: '木精', brand: '山峡', arrangeable: 32 }
+    const ethanolProd = { name: '乙醇', spec: '工业级', unit: '吨', pack: '槽罐', qtyPlan: 28, qtyActual: 27.8, method: '送货上门', alias: '酒精', brand: '某牌', arrangeable: 28 }
+    const aceticProd = { name: '冰醋酸', spec: '99.8%', unit: '吨', pack: '桶装', qtyPlan: 20, qtyActual: '—', method: '客户自提', alias: '', brand: '南通', arrangeable: 20 }
+
+    const plans = [
+      {
+        id: 'p1', no: 'JH20260914003', type: '出库单', status: '待安排', tag: 'tag-warn', date: '2026-09-14',
+        plate: '未派车', people: '—', product: '甲醇 32 吨 · 安庆山峡 → 合肥某化', time: '09-14 14:00',
+        supplier: '安庆山峡仓储', buyer: '合肥某化', follower: '王调度',
+        tabKeys: ['pending_arrange', 'outbound', 'all'], followStatus: 'pending_arrange', transportStatus: 'pending',
+        canArrange: true, canEdit: true, canWait: true, canCancelWait: false, canVoid: true, arrangeLabel: '安排运输',
+        loadCustomer: '安庆山峡仓储', loadTime: '2026-09-14', loadAddr: '安庆市迎江区化工园 A3',
+        loadContact: '0556-1234567', loadNotes: '进门登记', loadContract: '内部客户(无合同)', loadDibang: '生成(入库)',
+        loadProducts: [methanolProd],
+        deliveries: [
+          { customer: '合肥某化', time: '2026-09-14', addr: '合肥经开区某某路 88 号', contact: '0551-88886666', notes: '需预约卸货', contract: 'HT202609-001', dibang: '未生成', products: [{ ...methanolProd, qtyPlan: 24, arrangeable: 24 }] },
+          { customer: '南京某精细化工', time: '2026-09-15', addr: '南京江北新区', contact: '025-66668888', notes: '分卸 8 吨', contract: 'HT202607-056', dibang: '未生成', products: [{ ...methanolProd, qtyPlan: 8, arrangeable: 8 }] }
+        ],
+        siteLinks: [], vehicleDetail: null, feedback: null, qty: { plan: 32, load: 32, del: 32, balanced: true },
+        hazardCertReady: false,
+        hazardProducts: [
+          {
+            name: '甲醇', standardName: '甲醇', hazard: 'toxic',
+            purchaseCert: '甲醇购买证.pdf',
+            transportCert: '',
+            legalUseCert: ''
+          }
+        ]
+      },
+      {
+        id: 'p2', no: 'JH20260914001', type: '入库单', status: '已安排', tag: 'tag-ok', date: '2026-09-14',
+        plate: '皖H·D1234', people: '张伟 / 李强', product: '乙醇 28 吨 · 张家港 → 安庆仓', time: '09-14 10:30',
+        supplier: '张家港某仓', buyer: '安庆山峡仓储', follower: '李调度',
+        tabKeys: ['arranged', 'inbound', 'all'], followStatus: 'arranged', transportStatus: 'arranged',
+        canArrange: true, canEdit: true, canWait: false, canCancelWait: false, canVoid: true, arrangeLabel: '调整运输',
+        loadCustomer: '张家港某仓', loadTime: '2026-09-14', loadAddr: '张家港保税区',
+        loadContact: '0512-88880000', loadNotes: '危化通行证', loadContract: 'HT202608-118', loadDibang: '生成(出库)',
+        loadProducts: [ethanolProd],
+        deliveries: [
+          { customer: '安庆山峡仓储', time: '2026-09-14', addr: '安庆市迎江区化工园 A3', contact: '0556-1234567', notes: '入库过磅', contract: '内部客户(无合同)', dibang: '生成(入库)', products: [ethanolProd] }
+        ],
+        siteLinks: [
+          { label: '装货现场', url: 'https://w.example.com/h5/site?t=load***' },
+          { label: '送货现场', url: 'https://w.example.com/h5/site?t=del***' }
+        ],
+        vehicleDetail: { plate: '皖H·D1234', trailer: '皖H·G88', type: '危化品', loadTon: 34, carrier: '安庆某物流', driver: '张伟', driverPhone: '13800001111', escort: '李强', escortPhone: '13900002222' },
+        feedback: { driver: '装货已签到 · 预计 16:30 到场 · 实装 27.8 吨', site: '现场已确认到场 · 地磅入库单 DB20260914088 已生成' },
+        qty: { plan: 28, load: 28, del: 28, balanced: true },
+        hazardCertReady: true,
+        hazardProducts: [
+          {
+            name: '乙醇', standardName: '乙醇', hazard: 'toxic',
+            purchaseCert: '乙醇购买证.pdf',
+            transportCert: '乙醇运输证.pdf',
+            legalUseCert: ''
+          }
+        ]
+      },
+      {
+        id: 'p3', no: 'JH20260913018', type: '直销单', status: '等通知', tag: 'tag-wait', date: '2026-09-13',
+        plate: '苏E·5M892', people: '陈师傅', product: '冰醋酸 20 吨 · 南通 → 芜湖', time: '待通知',
+        supplier: '南通某厂', buyer: '芜湖港务', follower: '赵跟单',
+        tabKeys: ['pending_arrange', 'direct', 'all'], followStatus: 'pending_arrange', transportStatus: 'pending',
+        canArrange: true, canEdit: true, canWait: false, canCancelWait: true, canVoid: true, arrangeLabel: '安排运输',
+        loadCustomer: '南通某厂', loadTime: '待通知', loadAddr: '南通港区',
+        loadContact: '0513-70001111', loadNotes: '等客户通知后装车', loadContract: 'HT202607-056', loadDibang: '未生成',
+        loadProducts: [aceticProd],
+        deliveries: [
+          { customer: '芜湖港务', time: '待通知', addr: '芜湖鸠江区港口路', contact: '0553-22223333', notes: '港区预约', contract: 'HT202608-118', dibang: '未生成', products: [aceticProd] }
+        ],
+        siteLinks: [], vehicleDetail: { plate: '苏E·5M892', trailer: '无', type: '普货', loadTon: 20, carrier: '货拉拉(临调)', driver: '陈师傅', driverPhone: '13700003333', escort: '—', escortPhone: '—' },
+        feedback: null, qty: { plan: 20, load: 20, del: 20, balanced: true },
+        hazardCertReady: false,
+        hazardProducts: [
+          {
+            name: '冰醋酸', standardName: '冰醋酸', hazard: 'explosive',
+            purchaseCert: '',
+            transportCert: '',
+            legalUseCert: ''
+          }
+        ]
+      },
+      {
+        id: 'p4', no: 'JH20260912007', type: '出库单', status: '草稿中', tag: 'tag-mute', date: '2026-09-12',
+        plate: '—', people: '—', product: '待完善产品行', time: '—',
+        supplier: '—', buyer: '—', follower: '王调度',
+        tabKeys: ['drafting', 'outbound', 'all'], followStatus: 'drafting', transportStatus: 'pending',
+        canArrange: false, canEdit: true, canWait: false, canCancelWait: false, canVoid: true, arrangeLabel: '安排运输',
+        loadCustomer: '—', loadTime: '—', loadAddr: '—', loadContact: '—', loadNotes: '—', loadContract: '—', loadDibang: '—',
+        loadProducts: [], deliveries: [], siteLinks: [], vehicleDetail: null, feedback: null,
+        qty: { plan: 0, load: 0, del: 0, balanced: false },
+        hazardCertReady: true,
+        hazardProducts: []
+      }
+    ]
+
+    const vehicles = [
+      { id: 'v1', t: '皖H·D1234', d: '挂 皖H·G88 · 危化品' },
+      { id: 'v2', t: '苏E·5M892', d: '无挂车 · 普货' },
+      { id: 'v3', t: '皖A·88K21', d: '挂 皖A·G12 · 危化品' },
+      { id: 'v4', t: '皖H·F6688', d: '挂 皖H·G66 · 危化品' },
+      { id: 'v5', t: '浙B·9C321', d: '无挂车 · 普货' }
+    ]
+    const drivers = [
+      { id: 'd1', t: '张伟', d: '13800001111 · 司机' },
+      { id: 'd2', t: '陈师傅', d: '13700003333 · 司机' },
+      { id: 'd3', t: '刘洋', d: '13600005555 · 司机' },
+      { id: 'd4', t: '赵磊', d: '13500006666 · 司机' }
+    ]
+    const escorts = [
+      { id: 'e1', t: '李强', d: '13900002222 · 押运' },
+      { id: 'e2', t: '王磊', d: '13600004444 · 押运' },
+      { id: 'e3', t: '周敏', d: '13300007777 · 押运' }
+    ]
+    const customers = [
+      { id: 'c1', t: '安庆山峡仓储', d: '安庆迎江区化工园 A3' },
+      { id: 'c2', t: '合肥某化', d: '合肥经开区某某路 88 号' },
+      { id: 'c3', t: '张家港某仓', d: '张家港保税区' },
+      { id: 'c4', t: '芜湖港务', d: '芜湖鸠江区港口路' },
+      { id: 'c5', t: '南京某精细化工', d: '南京江北新区' }
+    ]
+    const products = [
+      { id: 'pr1', t: '甲醇 · 32 吨 · 吨袋', d: '危化品' },
+      { id: 'pr2', t: '乙醇 · 28 吨 · 槽罐', d: '危化品' },
+      { id: 'pr3', t: '冰醋酸 · 20 吨 · 桶装', d: '危化品' },
+      { id: 'pr4', t: '液碱 · 30 吨 · 槽罐', d: '危化品' },
+      { id: 'pr5', t: '盐酸 · 25 吨 · 槽罐', d: '危化品' }
+    ]
+    const carriers = [
+      { id: 'ca1', t: '安庆某物流', d: '三方承运商' },
+      { id: 'ca2', t: '合肥危运公司', d: '三方承运商' },
+      { id: 'ca3', t: '货拉拉(临调)', d: '临调' },
+      { id: 'ca4', t: '芜湖顺达运输', d: '三方承运商' },
+      { id: 'ca5', t: '南京危化专运', d: '三方承运商' },
+      { id: 'ca6', t: '安庆山峡自有车队', d: '自有' }
+    ]
+    const contracts = [
+      { id: 'ct1', t: 'HT202609-001', d: '合肥某化 · 甲醇销售' },
+      { id: 'ct2', t: 'HT202608-118', d: '芜湖港务 · 乙醇采购' },
+      { id: 'ct3', t: 'HT202607-056', d: '南京某精细化工 · 冰醋酸' }
+    ]
+    const addresses = [
+      { id: 'a1', t: '安庆市迎江区化工园 A3', d: '安庆山峡仓储 · 0556-1234567' },
+      { id: 'a2', t: '合肥经开区某某路 88 号', d: '合肥某化 · 0551-88886666' },
+      { id: 'a3', t: '张家港保税区', d: '张家港某仓 · 0512-88880000' },
+      { id: 'a4', t: '芜湖鸠江区港口路', d: '芜湖港务 · 0553-22223333' },
+      { id: 'a5', t: '南京江北新区', d: '南京某精细化工 · 025-66668888' }
+    ]
+
+    const state = {
+      screen: 'list',
+      history: [],
+      tab: 'pending_arrange',
+      date: 'today',
+      customDateLabel: '',
+      listPage: 1,
+      listPageSize: 2,
+      currentPlanId: 'p1',
+      deliveryTabIndex: 0,
+      formMode: 'new', // new | edit | arrange | copy
+      personnelRole: '司机',
+      pickerTarget: '',
+      confirmKind: '',
+      printKind: 'inbound',
+      printCustomer: '',
+      listMorePlanId: '',
+      stopCount: { load: 1, delivery: 1 },
+      pasteParsed: false,
+      pasteConflict: 'reuse',
+      picker: {
+        sheetId: '',
+        containerId: '',
+        pickAction: '',
+        items: [],
+        keyword: ''
+      }
+    }
+
+    const $ = (sel, root = document) => root.querySelector(sel)
+    const $$ = (sel, root = document) => Array.from(root.querySelectorAll(sel))
+
+    function toast(msg) {
+      const el = $('#toast')
+      el.textContent = msg
+      el.classList.add('show')
+      clearTimeout(toast._t)
+      toast._t = setTimeout(() => el.classList.remove('show'), 1800)
+    }
+
+    function currentPlan() {
+      return plans.find((p) => p.id === state.currentPlanId) || plans[0]
+    }
+
+    function closeSheet() {
+      $('#sheet-mask').classList.remove('open')
+      $$('.sheet').forEach((s) => s.classList.remove('open'))
+    }
+
+    function openSheet(id) {
+      closeSheet()
+      $('#sheet-mask').classList.add('open')
+      const el = document.getElementById(id)
+      if (el) el.classList.add('open')
+    }
+
+    function closeFilter() {
+      $('#filter-mask')?.classList.remove('open')
+      $$('.filter-panel').forEach((p) => p.classList.remove('open'))
+      $$('.filter-trig').forEach((t) => t.classList.remove('open'))
+    }
+
+    function closeSpeed() {
+      $('#speed-dial')?.classList.remove('open')
+      $('#speed-mask')?.classList.remove('open')
+    }
+
+    function openFilter(kind) {
+      closeSpeed()
+      const isOpen = $('#panel-' + kind)?.classList.contains('open')
+      closeFilter()
+      if (isOpen) return
+      $('#panel-' + kind)?.classList.add('open')
+      $('#trig-' + kind)?.classList.add('open')
+      $('#filter-mask')?.classList.add('open')
+    }
+
+    function showScreen(name, { push = true } = {}) {
+      closeSheet()
+      closeFilter()
+      closeSpeed()
+      if (push && state.screen !== name) state.history.push(state.screen)
+      state.screen = name
+      $$('.screen').forEach((s) => s.classList.toggle('active', s.id === 'screen-' + name))
+      if (name === 'detail') renderDetail()
+      if (name === 'form') renderForm()
+      if (name === 'print') renderPrint()
+      if (name === 'list') renderList()
+    }
+
+    function goBack() {
+      const prev = state.history.pop()
+      if (prev) showScreen(prev, { push: false })
+      else showScreen('list', { push: false })
+    }
+
+    function getRequiredCertSlots(product) {
+      if (product.hazard === 'explosive') {
+        return [{ key: 'legalUseCert', label: '合法证明' }]
+      }
+      // 易制毒(与 Web getRequiredCertSlots 一致)
+      return [
+        { key: 'purchaseCert', label: '购买证' },
+        { key: 'transportCert', label: '运输证' }
+      ]
+    }
+
+    function isHazardProductCertComplete(product) {
+      return getRequiredCertSlots(product).every((slot) => String(product[slot.key] || '').trim())
+    }
+
+    function renderHazardCert(p) {
+      const list = $('#d-cert-list')
+      const empty = $('#d-cert-empty')
+      const hint = $('#d-cert-hint')
+      const summaryText = $('#d-cert-summary-text')
+      const readyTag = $('#d-cert-ready-tag')
+      const products = p.hazardProducts || []
+
+      if (!products.length) {
+        list.innerHTML = ''
+        empty.classList.remove('hidden')
+        hint.classList.add('hidden')
+        summaryText.textContent = '本计划无易制毒/易制爆产品'
+        readyTag.className = 'tag tag-mute'
+        readyTag.textContent = '无需证书'
+        return
+      }
+
+      empty.classList.add('hidden')
+      hint.classList.remove('hidden')
+      const allOk = products.every(isHazardProductCertComplete)
+      const ready = !!p.hazardCertReady && allOk
+      readyTag.className = 'tag ' + (ready ? 'tag-ok' : 'tag-warn')
+      readyTag.textContent = ready ? '已齐备' : '待确认'
+      const missingCount = products.reduce((n, prod) => {
+        return n + getRequiredCertSlots(prod).filter((s) => !String(prod[s.key] || '').trim()).length
+      }, 0)
+      summaryText.textContent = missingCount
+        ? `${products.length} 个危化产品 · ${missingCount} 项证书未上传`
+        : `${products.length} 个危化产品 · 附件已齐,待业务确认`
+
+      list.innerHTML = products.map((prod, index) => {
+        const mark = prod.hazard === 'explosive'
+          ? '<span class="cert-mark explosive">爆</span>'
+          : '<span class="cert-mark toxic">毒</span>'
+        const slots = getRequiredCertSlots(prod).map((slot) => {
+          const file = String(prod[slot.key] || '').trim()
+          const body = file
+            ? `<button class="cert-file" type="button" data-action="toast" data-msg="预览附件:${file}(Demo)"><span>📎 ${file}</span></button>`
+            : '<span class="cert-missing">未上传</span>'
+          return `
+            <div class="cert-slot">
+              <span class="cert-slot__label">${slot.label}</span>
+              <div class="cert-slot__body">${body}</div>
+            </div>
+          `
+        }).join('')
+        return `
+          <div class="cert-block">
+            <div class="cert-block__head">
+              <span class="cert-block__index">${index + 1}.</span>
+              <span class="cert-block__name">${prod.standardName || prod.name}</span>
+              ${mark}
+            </div>
+            <div class="cert-slots">${slots}</div>
+          </div>
+        `
+      }).join('')
+    }
+
+    function renderProdTable(hostId, rows) {
+      const host = document.getElementById(hostId)
+      if (!host) return
+      if (!rows || !rows.length) {
+        host.innerHTML = '<div class="prod-row muted">暂无产品</div>'
+        return
+      }
+      host.innerHTML = rows.map((r) => `
+        <div class="prod-row">
+          <div class="prod-row__name">${r.name || '—'}${r.spec ? ' · ' + r.spec : ''}</div>
+          <div class="prod-row__grid">
+            <div>单位 <b>${r.unit || '—'}</b></div>
+            <div>包装 <b>${r.pack || '—'}</b></div>
+            <div>计划量 <b>${r.qtyPlan ?? '—'}</b></div>
+            <div>实际量 <b>${r.qtyActual ?? '—'}</b></div>
+            ${r.method ? `<div>送货方式 <b>${r.method}</b></div>` : ''}
+            ${r.brand ? `<div>品牌 <b>${r.brand}</b></div>` : ''}
+            ${r.alias ? `<div>别名 <b>${r.alias}</b></div>` : ''}
+            ${r.arrangeable != null ? `<div>可安排 <b>${r.arrangeable}</b></div>` : ''}
+          </div>
+        </div>
+      `).join('')
+    }
+
+    function renderDeliveryTab(p, idx) {
+      const list = p.deliveries || []
+      const d = list[idx] || list[0]
+      const tabs = $('#d-delivery-tabs')
+      if (!tabs) return
+      if (!list.length) {
+        tabs.innerHTML = ''
+        $('#d-del-customer').textContent = '—'
+        $('#d-del-time').textContent = '—'
+        $('#d-del-addr').textContent = '—'
+        $('#d-del-contact').textContent = '—'
+        $('#d-del-notes').textContent = '—'
+        $('#d-del-contract').textContent = '—'
+        $('#d-del-dibang').textContent = '—'
+        renderProdTable('d-del-products', [])
+        return
+      }
+      tabs.innerHTML = list.map((item, i) =>
+        `<button class="delivery-tab ${i === idx ? 'active' : ''}" type="button" data-action="pick-delivery-tab" data-idx="${i}">送货段 ${i + 1}</button>`
+      ).join('')
+      $('#d-del-customer').textContent = d.customer || '—'
+      $('#d-del-time').textContent = d.time || '—'
+      $('#d-del-addr').textContent = d.addr || '—'
+      $('#d-del-contact').textContent = d.contact || '—'
+      $('#d-del-notes').textContent = d.notes || '—'
+      $('#d-del-contract').textContent = d.contract || '—'
+      $('#d-del-dibang').textContent = d.dibang || '—'
+      renderProdTable('d-del-products', d.products || [])
+    }
+
+    function renderList() {
+      const kw = ($('#list-keyword').value || '').trim()
+      const rows = plans.filter((p) => {
+        if (state.tab !== 'all' && !p.tabKeys.includes(state.tab)) return false
+        if (!kw) return true
+        const blob = [p.no, p.type, p.plate, p.people, p.product, p.supplier, p.buyer, p.follower].join(' ')
+        return blob.includes(kw)
+      })
+      const list = $('#plan-list')
+      const btnMore = $('#btn-load-more')
+      if (!rows.length) {
+        list.innerHTML = '<div class="empty">该筛选条件下暂无计划</div>'
+        if (btnMore) btnMore.style.display = 'none'
+        return
+      }
+      const visible = rows.slice(0, state.listPage * state.listPageSize)
+      list.innerHTML = visible.map((p) => `
+        <article class="card" data-action="open-detail" data-id="${p.id}">
+          <div class="card-head">
+            <div>
+              <div class="plan-no">${p.no}</div>
+              <div class="muted">${p.type} · ${p.date}</div>
+            </div>
+            <span class="tag ${p.tag}">${p.status}</span>
+          </div>
+          <div class="meta">
+            <div>供方 → 需方:<b>${p.supplier || '—'} → ${p.buyer || '—'}</b></div>
+            <div>跟进人:<b>${p.follower || '—'}</b></div>
+            <div>车牌:<b>${p.plate}</b></div>
+            <div>人员:<b>${p.people}</b></div>
+          </div>
+          <div class="products">${p.product}</div>
+          <div class="card-actions">
+            <button class="btn" type="button" data-action="open-list-more" data-id="${p.id}">更多</button>
+            <button class="btn" type="button" data-action="open-form" data-mode="copy" data-id="${p.id}">复制</button>
+            ${p.canArrange ? `<button class="btn btn-primary" type="button" data-action="open-form" data-mode="arrange" data-id="${p.id}">${p.arrangeLabel}</button>` : ''}
+          </div>
+        </article>
+      `).join('')
+      if (btnMore) {
+        const hasMore = visible.length < rows.length
+        btnMore.style.display = hasMore ? 'block' : 'none'
+        btnMore.textContent = hasMore ? `加载更多(${visible.length}/${rows.length})` : '没有更多了'
+      }
+    }
+
+    function renderDetail() {
+      const p = currentPlan()
+      $('#detail-no').textContent = p.no
+      $('#detail-sub').textContent = `${p.type} · 计划日期 ${p.date}`
+      const tag = $('#detail-tag')
+      tag.className = 'tag ' + p.tag
+      tag.textContent = p.status
+
+      $('#d-plan-no').textContent = p.no
+      $('#d-plan-date').textContent = p.date
+      $('#d-company').textContent = '安庆山峡'
+      $('#d-type').textContent = p.type
+      $('#d-follow').textContent = p.follower || '王调度'
+
+      const arranged = p.transportStatus === 'arranged'
+      const hasVehicle = p.plate && p.plate !== '未派车' && p.plate !== '—'
+      $('#d-arrange-links').classList.toggle('hidden', !arranged)
+      $('#d-vehicle-block').classList.toggle('hidden', !hasVehicle && !arranged)
+      $('#d-vehicle-empty').classList.toggle('hidden', !!(hasVehicle || arranged))
+      $('#d-feedback')?.classList.toggle('hidden', !(arranged && p.feedback))
+      if (arranged) {
+        const links = $('#d-site-links')
+        const siteLinks = p.siteLinks && p.siteLinks.length
+          ? p.siteLinks
+          : [{ label: '现场链接', url: 'https://w.example.com/h5/site?t=***' }]
+        links.innerHTML = siteLinks.map((s) => `
+          <div class="link-row">
+            <input class="control" readonly value="${s.url}" />
+            <button class="btn" type="button" data-action="toast" data-msg="已复制${s.label}">复制</button>
+          </div>
+          <div class="muted" style="margin:-4px 0 6px">${s.label}</div>
+        `).join('')
+      }
+      if (hasVehicle || arranged) {
+        const vd = p.vehicleDetail
+        $('#d-v-plate').textContent = vd?.plate || (hasVehicle ? p.plate : '皖H·D1234')
+        $('#d-v-trailer').textContent = vd ? `挂 ${vd.trailer}` : (hasVehicle ? '挂 皖H·G88' : '挂 —')
+        const people = p.people && p.people !== '—' ? p.people : '张伟 / 李强'
+        const parts = people.split(/[\/·]/).map((s) => s.trim()).filter(Boolean)
+        $('#d-v-people').textContent =
+          `司机 ${vd?.driver || parts[0] || '—'} · 押运 ${vd?.escort || parts[1] || '—'}`
+        $('#d-v-carrier').textContent = `承运商 ${vd?.carrier || (arranged ? '安庆某物流' : '—')}`
+        $('#d-v-view')?.classList.toggle('hidden', !vd && !hasVehicle)
+      }
+      if (p.feedback) {
+        $('#d-fb-driver').textContent = p.feedback.driver || '—'
+        $('#d-fb-site').textContent = p.feedback.site || '—'
+      }
+
+      $('#d-load-customer').textContent = p.loadCustomer || '—'
+      $('#d-load-time').textContent = p.loadTime || '—'
+      $('#d-load-addr').textContent = p.loadAddr || '—'
+      $('#d-load-contact').textContent = p.loadContact || '—'
+      $('#d-load-notes').textContent = p.loadNotes || '—'
+      $('#d-load-contract').textContent = p.loadContract || '—'
+      $('#d-load-dibang').textContent = p.loadDibang || '—'
+      renderProdTable('d-load-products', p.loadProducts || [])
+      if (state.deliveryTabIndex >= (p.deliveries || []).length) state.deliveryTabIndex = 0
+      renderDeliveryTab(p, state.deliveryTabIndex)
+
+      const q = p.qty || { plan: 0, load: 0, del: 0, balanced: true }
+      $('#d-qty-plan').textContent = String(q.plan)
+      $('#d-qty-load').textContent = String(q.load)
+      $('#d-qty-del').textContent = String(q.del)
+      if (q.balanced) {
+        $('#d-qty-balance').textContent = '✓ 数量平衡'
+        $('#d-qty-balance').className = 'qty-ok'
+      } else {
+        $('#d-qty-balance').textContent = p.id === 'p4' ? '数量不平衡(可暂存)' : '⚠ 装送数量不一致'
+        $('#d-qty-balance').className = ''
+      }
+
+      renderHazardCert(p)
+
+      if (p.id === 'p4') {
+        $('#d-freight-has').textContent = '否'
+        ;['d-freight-km','d-freight-type','d-freight-qty','d-freight-price','d-freight-amount','d-freight-invoice','d-freight-remark'].forEach((id) => {
+          const el = document.getElementById(id)
+          if (el) el.textContent = '—'
+        })
+        $('#d-remark').textContent = '—'
+      } else {
+        $('#d-freight-has').textContent = arranged ? '是' : '否'
+        if (arranged) {
+          $('#d-freight-km').textContent = '186'
+          $('#d-freight-type').textContent = '按重量'
+          $('#d-freight-qty').textContent = String(q.plan || 28)
+          $('#d-freight-price').textContent = '120.00'
+          $('#d-freight-amount').textContent = '3,840.00'
+          $('#d-freight-invoice').textContent = '是'
+          $('#d-freight-remark').textContent = '含过路费'
+        } else {
+          ;['d-freight-km','d-freight-type','d-freight-qty','d-freight-price','d-freight-amount','d-freight-invoice','d-freight-remark'].forEach((id) => {
+            const el = document.getElementById(id)
+            if (el) el.textContent = '—'
+          })
+        }
+        $('#d-remark').textContent = '注意厂区进门登记'
+      }
+
+      const primary = []
+      const more = []
+      if (p.canEdit) {
+        primary.push({
+          html: `<button class="btn btn-primary" type="button" data-action="open-form" data-mode="edit" data-id="${p.id}">编辑</button>`
+        })
+      }
+      if (p.canArrange) {
+        primary.push({
+          html: `<button class="btn ${p.canEdit ? '' : 'btn-primary'}" type="button" data-action="open-form" data-mode="arrange" data-id="${p.id}">${p.arrangeLabel}</button>`
+        })
+      }
+      more.push({
+        html: `<button class="more-action" type="button" data-action="open-form" data-mode="copy" data-id="${p.id}">复制</button>`
+      })
+      if (p.canWait) {
+        more.push({
+          html: `<button class="more-action" type="button" data-action="confirm" data-kind="wait">等客户通知</button>`
+        })
+      }
+      if (p.canCancelWait) {
+        more.push({
+          html: `<button class="more-action" type="button" data-action="confirm" data-kind="cancel-wait">取消等通知</button>`
+        })
+      }
+      more.push({
+        html: `<button class="more-action" type="button" data-nav="print">打印单据</button>`
+      })
+      if (p.canVoid) {
+        more.push({
+          html: `<button class="more-action danger" type="button" data-action="confirm" data-kind="void">作废</button>`
+        })
+      }
+
+      while (primary.length > 2) more.unshift(primary.pop())
+      while (primary.length < 2 && more.length) {
+        const item = more.shift()
+        const promoted = item.html
+          .replace('class="more-action danger"', 'class="btn btn-danger"')
+          .replace('class="more-action"', 'class="btn"')
+        primary.push({ html: promoted })
+      }
+
+      const fab = []
+      if (more.length) {
+        fab.push(`<button class="btn btn-more" type="button" data-action="open-detail-more" aria-label="更多">⋯</button>`)
+      }
+      primary.forEach((item) => fab.push(item.html))
+      $('#detail-fab').innerHTML = fab.join('')
+
+      const moreHost = $('#detail-more-options')
+      if (moreHost) {
+        moreHost.innerHTML = more.length
+          ? more.map((item) => item.html).join('')
+          : '<div class="sheet-empty">暂无更多操作</div>'
+      }
+    }
+
+    function renderForm() {
+      const titles = {
+        new: '新增运输计划',
+        edit: '编辑运输计划',
+        arrange: '安排运输',
+        copy: '新增运输计划'
+      }
+      const hints = {
+        new: '对齐 Web 新增:取消 / 暂存 / 创建 / 创建并安排运输',
+        edit: '对齐 Web 编辑:按跟进/运输状态显示暂存、保存、保存并安排',
+        arrange: '对齐 Web 安排/调整:同一表单,openForArrange 模式',
+        copy: '对齐 Web 复制:等同新增(预填来源计划)'
+      }
+      $('#form-title').textContent = titles[state.formMode] || titles.new
+      $('#form-hint').textContent = hints[state.formMode] || hints.new
+      const p = currentPlan()
+      if (state.formMode !== 'new' && state.formMode !== 'copy') {
+        $('#form-sub').textContent = `${p.no} · ${p.type} · ${p.status}`
+      } else {
+        $('#form-sub').textContent = '长页分区 · 锚点跳转 · 按钮对齐 Web 弹窗'
+      }
+
+      // 对齐 PlPlanFormDialog 底栏显隐规则
+      const isNewLike = state.formMode === 'new' || state.formMode === 'copy'
+      const isArrange = state.formMode === 'arrange'
+      const isRearrange = isArrange && p.transportStatus === 'arranged'
+      const showDraft =
+        isNewLike ||
+        (!isRearrange &&
+          (p.followStatus === 'drafting' || p.followStatus === 'pending_arrange'))
+      const showSave = isNewLike || (!isRearrange && p.transportStatus !== 'arranged')
+      const showSaveAndArrange =
+        isNewLike || (!isRearrange && (p.transportStatus === 'pending' || p.transportStatus === 'arranged'))
+      const showRearrange = isRearrange
+
+      const fab = []
+      fab.push(`<button class="btn" type="button" data-action="back">取消</button>`)
+      if (showDraft) {
+        fab.push(`<button class="btn" type="button" data-action="form-draft">暂存</button>`)
+      }
+      if (showSave) {
+        const saveLabel = isNewLike ? '创建' : '保存'
+        const saveCls = showSaveAndArrange || showRearrange ? 'btn' : 'btn btn-primary'
+        fab.push(
+          `<button class="${saveCls}" type="button" data-action="form-save">${saveLabel}</button>`
+        )
+      }
+      if (showSaveAndArrange) {
+        const arrangeLabel = isNewLike ? '创建并安排运输' : '保存并安排'
+        fab.push(
+          `<button class="btn btn-primary" type="button" data-action="form-save-arrange">${arrangeLabel}</button>`
+        )
+      }
+      if (showRearrange) {
+        fab.push(
+          `<button class="btn btn-primary" type="button" data-action="form-rearrange">调整运输</button>`
+        )
+      }
+      $('#form-fab').innerHTML = fab.join('')
+
+      if (isArrange) {
+        setTimeout(() => scrollToSection('sec-transport'), 60)
+      }
+    }
+
+    function scrollToSection(id) {
+      const host = $('#form-scroll')
+      const el = document.getElementById(id)
+      if (!host || !el) return
+      host.scrollTo({ top: el.offsetTop - 8, behavior: 'smooth' })
+      $$('#anchor-bar .anchor').forEach((a) => a.classList.toggle('active', a.dataset.anchor === id))
+    }
+
+    function renderPrint() {
+      const p = currentPlan()
+      const kindLabel = {
+        inbound: '入库单',
+        delivery: '送(提)货单',
+        subpack: '分装单',
+        'blank-inbound': '空白入库单',
+        'blank-delivery': '空白送(提)货单',
+        'blank-subpack': '空白分装单'
+      }[state.printKind] || '单据'
+      const needCustomer = state.printKind === 'delivery' || state.printKind === 'blank-delivery'
+      const block = $('#print-customer-block')
+      if (block) block.style.display = needCustomer ? 'block' : 'none'
+      if (!state.printCustomer && (p.deliveries || []).length) {
+        state.printCustomer = p.deliveries[0].customer
+      }
+      const custLabel = $('#print-customer-label')
+      if (custLabel) {
+        custLabel.textContent = state.printCustomer || '请选择送货客户'
+        if (!state.printCustomer) custLabel.innerHTML = '<span class="ph">请选择送货客户</span>'
+      }
+      const custLine = needCustomer
+        ? `<div class="line"><span>送货客户</span><span>${state.printCustomer || '未选择'}</span></div>`
+        : ''
+      $('#print-preview').innerHTML = `
+        <h2>${kindLabel}</h2>
+        <div class="line"><span>计划编号</span><span>${p.no}</span></div>
+        <div class="line"><span>单据类型</span><span>${p.type}</span></div>
+        <div class="line"><span>计划日期</span><span>${p.date}</span></div>
+        ${custLine}
+        <div class="line"><span>供方 → 需方</span><span>${p.supplier || '—'} → ${p.buyer || '—'}</span></div>
+        <div class="line"><span>车牌</span><span>${p.plate}</span></div>
+        <div class="line"><span>人员</span><span>${p.people}</span></div>
+        <div class="line"><span>产品摘要</span><span>${p.product}</span></div>
+        <p style="margin-top:16px;color:#888;text-align:center">—— 预览示意 ——</p>
+      `
+    }
+
+    function fillOptions(containerId, items, onPickAttr) {
+      const el = document.getElementById(containerId)
+      if (!el) return
+      if (!items.length) {
+        el.innerHTML = '<div class="sheet-empty">无匹配结果</div>'
+        return
+      }
+      el.innerHTML = items.map((it) => `
+        <div class="option-row" data-action="${onPickAttr}" data-label="${it.t}" data-desc="${it.d || ''}">
+          <div><div class="t">${it.t}</div><div class="d">${it.d || ''}</div></div>
+          <span class="chev">选用</span>
+        </div>
+      `).join('')
+    }
+
+    function fuzzyMatch(item, keyword) {
+      const kw = (keyword || '').trim().toLowerCase()
+      if (!kw) return true
+      const blob = `${item.t || ''} ${item.d || ''}`.toLowerCase()
+      return kw.split(/\s+/).every((part) => blob.includes(part))
+    }
+
+    function renderPickerOptions() {
+      const { containerId, pickAction, items, keyword } = state.picker
+      if (!containerId) return
+      const filtered = (items || []).filter((it) => fuzzyMatch(it, keyword))
+      fillOptions(containerId, filtered, pickAction)
+    }
+
+    function openPickerSheet(sheetId, containerId, items, pickAction) {
+      state.picker = {
+        sheetId,
+        containerId,
+        pickAction,
+        items: items || [],
+        keyword: ''
+      }
+      const input = $(`#${sheetId} .sheet-search-input`)
+      if (input) input.value = ''
+      renderPickerOptions()
+      openSheet(sheetId)
+      if (input) setTimeout(() => input.focus(), 220)
+    }
+
+    function openConfirm(kind) {
+      state.confirmKind = kind
+      if (kind === 'wait') {
+        $('#confirm-title').textContent = '等客户通知'
+        $('#confirm-msg').textContent = '确认将该计划标记为等客户通知?'
+        $('#confirm-ok').className = 'btn btn-primary'
+        $('#confirm-ok').textContent = '确认'
+      } else if (kind === 'cancel-wait') {
+        $('#confirm-title').textContent = '取消等通知'
+        $('#confirm-msg').textContent = '确认取消等通知,恢复为待安排?'
+        $('#confirm-ok').className = 'btn btn-primary'
+        $('#confirm-ok').textContent = '确认'
+      } else if (kind === 'void') {
+        $('#confirm-title').textContent = '作废运输计划'
+        $('#confirm-msg').textContent = '确认作废该运输计划吗?作废后将无法恢复。'
+        $('#confirm-ok').className = 'btn btn-danger'
+        $('#confirm-ok').textContent = '确认作废'
+      }
+      openSheet('sheet-confirm')
+    }
+
+    function addStop(side) {
+      if (side !== 'delivery') {
+        toast('装货信息与 Web 一致为单段,请编辑上方装货区')
+        return
+      }
+      state.stopCount.delivery += 1
+      const n = state.stopCount.delivery
+      const host = $('#delivery-stops')
+      const idC = `del-c${n}`
+      const idCt = `del-ct${n}`
+      const idP = `del-p${n}`
+      const wrap = document.createElement('div')
+      wrap.className = 'stop-card'
+      wrap.dataset.side = 'delivery'
+      wrap.innerHTML = `
+        <div class="stop-head">
+          <b>送货段 ${n}</b>
+          <span>
+            <button class="link-btn" type="button" data-action="toast" data-msg="已上移(模拟)">上移</button>
+            <button class="link-btn" type="button" data-action="toast" data-msg="已下移(模拟)">下移</button>
+            <button class="link-btn danger" type="button" data-action="remove-stop">删除</button>
+          </span>
+        </div>
+        <div class="field">
+          <label>送货客户 <span style="color:var(--danger)">*</span></label>
+          <button class="picker" type="button" data-sheet="customer" data-target="${idC}">
+            <span id="${idC}"><span class="ph">请选择客户</span></span>
+            <span class="chev">选择</span>
+          </button>
+        </div>
+        <div class="field">
+          <label>送货合同</label>
+          <button class="picker" type="button" data-sheet="contract" data-target="${idCt}">
+            <span id="${idCt}"><span class="ph">请选择合同</span></span>
+            <span class="chev">选择</span>
+          </button>
+        </div>
+        <div class="field-row">
+          <div class="field">
+            <label>送货时间 <span style="color:var(--danger)">*</span></label>
+            <input class="control" type="date" value="2026-09-14" />
+          </div>
+          <div class="field">
+            <label>联系方式</label>
+            <input class="control" placeholder="联系方式" />
+          </div>
+        </div>
+        <div class="field">
+          <label>送货地址</label>
+          <div class="link-row">
+            <input class="control" id="del-addr-${n}" placeholder="送货地址" />
+            <button class="btn" type="button" data-sheet="address" data-target="del-addr-${n}">地址库</button>
+          </div>
+        </div>
+        <div class="field">
+          <label>注意事项</label>
+          <input class="control" placeholder="送货注意事项" />
+        </div>
+        <div class="field">
+          <label>地磅单</label>
+          <label class="check-row"><input type="checkbox" /> 生成地磅单(出库)</label>
+        </div>
+        <div class="stop-head" style="margin-top:8px">
+          <b>送货产品</b>
+          <span>
+            <button class="link-btn" type="button" data-action="toast" data-msg="已从装货产品同步">从装货同步</button>
+            <button class="link-btn" type="button" data-sheet="product" data-target="${idP}">+ 选择产品</button>
+          </span>
+        </div>
+        <div class="product-card">
+          <div class="product-card__title" id="${idP}"><span class="ph">请选择产品</span></div>
+          <div class="field-row">
+            <div class="field">
+              <label>计划数量</label>
+              <input class="control" type="number" value="" placeholder="0" />
+            </div>
+            <div class="field">
+              <label>单位</label>
+              <select class="control"><option>吨</option></select>
+            </div>
+          </div>
+          <div class="field">
+            <label>包装规格</label>
+            <select class="control"><option>吨袋</option><option>槽罐</option></select>
+          </div>
+          <div class="field">
+            <label>送货方式</label>
+            <select class="control"><option>送货上门</option><option>客户自提</option></select>
+          </div>
+        </div>
+      `
+      host.appendChild(wrap)
+      toast(`已添加送货段 ${n}`)
+    }
+
+    function refreshVehicleSummary() {
+      const plate = ($('#f-plate')?.textContent || '').trim()
+      const driver = ($('#f-driver')?.textContent || '').trim()
+      const escort = ($('#f-escort')?.textContent || '').trim()
+      const carrier = ($('#f-carrier')?.textContent || '').trim()
+      const box = $('#f-vehicle-summary')
+      if (!box) return
+      const has = plate && !plate.includes('请选择')
+      box.style.display = has ? 'block' : 'none'
+      if (!has) return
+      $('#f-vs-plate').textContent = plate
+      $('#f-vs-people').textContent =
+        `司机 ${driver.includes('请选择') ? '—' : driver} · 押运 ${escort.includes('请选择') ? '—' : escort}`
+      $('#f-vs-carrier').textContent =
+        `承运商 ${carrier.includes('请选择') ? '—' : carrier}`
+    }
+
+    function addLoadProduct(name) {
+      const host = $('#load-products')
+      if (!host) return
+      const card = document.createElement('div')
+      card.className = 'product-card'
+      card.innerHTML = `
+        <div class="product-card__title">${name || '新产品'}</div>
+        <div class="field-row">
+          <div class="field">
+            <label>型号规格</label>
+            <input class="control" placeholder="型号规格" />
+          </div>
+          <div class="field">
+            <label>品牌</label>
+            <input class="control" placeholder="品牌" />
+          </div>
+        </div>
+        <div class="field-row">
+          <div class="field">
+            <label>别名</label>
+            <input class="control" placeholder="别名" />
+          </div>
+          <div class="field">
+            <label>可安排量</label>
+            <input class="control" value="0" readonly />
+          </div>
+        </div>
+        <div class="field-row">
+          <div class="field">
+            <label>计划数量 <span style="color:var(--danger)">*</span></label>
+            <input class="control" type="number" value="0" />
+          </div>
+          <div class="field">
+            <label>单位 <span style="color:var(--danger)">*</span></label>
+            <select class="control"><option>吨</option><option>千克</option></select>
+          </div>
+        </div>
+        <div class="field">
+          <label>包装规格 <span style="color:var(--danger)">*</span></label>
+          <select class="control"><option>吨袋</option><option>槽罐</option><option>桶装</option></select>
+        </div>
+        <button class="link-btn danger" type="button" data-action="remove-product">删除</button>
+      `
+      host.appendChild(card)
+    }
+
+    function openVehicleView() {
+      const p = currentPlan()
+      const vd = p.vehicleDetail || {
+        plate: p.plate, trailer: '—', type: '—', loadTon: '—', carrier: '—',
+        driver: (p.people || '—').split('/')[0]?.trim() || '—',
+        driverPhone: '—', escort: (p.people || '—').split('/')[1]?.trim() || '—', escortPhone: '—'
+      }
+      $('#vehicle-view-body').innerHTML = `
+        <div class="kv"><span>主车号</span><span>${vd.plate || '—'}</span></div>
+        <div class="kv"><span>挂车号</span><span>${vd.trailer || '—'}</span></div>
+        <div class="kv"><span>车辆类型</span><span>${vd.type || '—'}</span></div>
+        <div class="kv"><span>核载(吨)</span><span>${vd.loadTon ?? '—'}</span></div>
+        <div class="kv"><span>承运商</span><span>${vd.carrier || '—'}</span></div>
+        <div class="kv"><span>司机</span><span>${vd.driver || '—'} · ${vd.driverPhone || '—'}</span></div>
+        <div class="kv"><span>押运</span><span>${vd.escort || '—'} · ${vd.escortPhone || '—'}</span></div>
+      `
+      openSheet('sheet-vehicle-view')
+    }
+
+    function openListMore(planId) {
+      state.listMorePlanId = planId
+      const p = plans.find((x) => x.id === planId) || currentPlan()
+      const host = $('#list-more-options')
+      const items = []
+      items.push(`<button class="more-action" type="button" data-action="open-detail" data-id="${p.id}">查看详情</button>`)
+      items.push(`<button class="more-action" type="button" data-action="open-form" data-mode="copy" data-id="${p.id}">复制</button>`)
+      if (p.canWait) items.push(`<button class="more-action" type="button" data-action="confirm" data-kind="wait">等客户通知</button>`)
+      if (p.canCancelWait) items.push(`<button class="more-action" type="button" data-action="confirm" data-kind="cancel-wait">取消等通知</button>`)
+      if (p.canVoid) items.push(`<button class="more-action danger" type="button" data-action="confirm" data-kind="void">作废</button>`)
+      host.innerHTML = items.join('')
+      openSheet('sheet-list-more')
+    }
+
+    // events
+    $('#phone').addEventListener('click', (e) => {
+      const t = e.target.closest('[data-nav],[data-action],[data-sheet],[data-kind],[data-anchor]')
+      if (!t) return
+
+      if (t.dataset.nav) {
+        e.preventDefault()
+        closeSpeed()
+        closeFilter()
+        if (t.dataset.nav === 'form') {
+          state.formMode = t.dataset.mode || 'new'
+          showScreen('form')
+        } else {
+          showScreen(t.dataset.nav)
+        }
+        return
+      }
+
+      if (t.dataset.kind && t.closest('#print-kinds')) {
+        $$('#print-kinds .chip').forEach((x) => x.classList.remove('active'))
+        t.classList.add('active')
+        state.printKind = t.dataset.kind
+        renderPrint()
+        return
+      }
+
+      if (t.dataset.anchor) {
+        scrollToSection(t.dataset.anchor)
+        return
+      }
+
+      if (t.dataset.sheet) {
+        e.preventDefault()
+        state.pickerTarget = t.dataset.target || ''
+        if (t.dataset.sheet === 'vehicle') {
+          openPickerSheet('sheet-vehicle', 'vehicle-options', vehicles, 'pick-vehicle')
+        } else if (t.dataset.sheet === 'personnel') {
+          state.personnelRole = t.dataset.role || '司机'
+          $('#personnel-title').textContent = '选择' + state.personnelRole
+          openPickerSheet(
+            'sheet-personnel',
+            'personnel-options',
+            state.personnelRole === '押运' ? escorts : drivers,
+            'pick-personnel'
+          )
+        } else if (t.dataset.sheet === 'customer') {
+          openPickerSheet('sheet-customer', 'customer-options', customers, 'pick-customer')
+        } else if (t.dataset.sheet === 'product') {
+          openPickerSheet('sheet-product', 'product-options', products, 'pick-product')
+        } else if (t.dataset.sheet === 'carrier') {
+          openPickerSheet('sheet-carrier', 'carrier-options', carriers, 'pick-carrier')
+        } else if (t.dataset.sheet === 'contract') {
+          openPickerSheet('sheet-contract', 'contract-options', contracts, 'pick-contract')
+        } else if (t.dataset.sheet === 'address') {
+          openPickerSheet('sheet-address', 'address-options', addresses, 'pick-address')
+        }
+        return
+      }
+
+      const action = t.dataset.action
+      if (!action) return
+      e.preventDefault()
+
+      if (action === 'toast') toast(t.dataset.msg || '已点击')
+      if (action === 'back') goBack()
+      if (action === 'close-sheet') closeSheet()
+      if (action === 'close-filter') closeFilter()
+      if (action === 'close-speed') closeSpeed()
+      if (action === 'open-detail-more') {
+        state.listMorePlanId = ''
+        openSheet('sheet-detail-more')
+      }
+      if (action === 'toggle-speed') {
+        const dial = $('#speed-dial')
+        const open = dial.classList.toggle('open')
+        $('#speed-mask').classList.toggle('open', open)
+        if (open) closeFilter()
+      }
+      if (action === 'toggle-filter') {
+        openFilter(t.dataset.filter)
+      }
+      if (action === 'pick-date') {
+        state.date = t.dataset.date
+        $$('#panel-date .filter-opt').forEach((o) => o.classList.toggle('active', o === t))
+        const customRow = $('#custom-date-row')
+        if (t.dataset.date === 'custom') {
+          customRow?.classList.remove('hidden')
+          $('#label-date').textContent = state.customDateLabel || '自定义'
+          return
+        }
+        customRow?.classList.add('hidden')
+        $('#label-date').textContent = t.dataset.label
+        closeFilter()
+        state.listPage = 1
+        renderList()
+        toast('已切换日期:' + t.dataset.label)
+      }
+      if (action === 'apply-custom-date') {
+        const from = $('#custom-date-from')?.value || ''
+        const to = $('#custom-date-to')?.value || ''
+        state.date = 'custom'
+        state.customDateLabel = `${from} ~ ${to}`
+        $('#label-date').textContent = state.customDateLabel
+        closeFilter()
+        state.listPage = 1
+        renderList()
+        toast('已应用自定义日期')
+      }
+      if (action === 'load-more') {
+        state.listPage += 1
+        renderList()
+        toast('已加载更多(模拟)')
+      }
+      if (action === 'pick-delivery-tab') {
+        state.deliveryTabIndex = Number(t.dataset.idx || 0)
+        renderDeliveryTab(currentPlan(), state.deliveryTabIndex)
+      }
+      if (action === 'open-vehicle-view') openVehicleView()
+      if (action === 'open-list-more') openListMore(t.dataset.id)
+      if (action === 'pick-print-customer-open') {
+        const p = currentPlan()
+        const items = (p.deliveries || []).map((d, i) => ({ id: 'pc' + i, t: d.customer, d: d.addr || '' }))
+        if (!items.length) {
+          toast('当前计划无送货客户')
+          return
+        }
+        openPickerSheet('sheet-customer', 'customer-options', items, 'pick-print-customer')
+      }
+      if (action === 'pick-tab') {
+        state.tab = t.dataset.tab
+        $('#label-tab').textContent = t.dataset.label
+        $$('#panel-tab .filter-opt').forEach((o) => o.classList.toggle('active', o === t))
+        closeFilter()
+        state.listPage = 1
+        renderList()
+      }
+      if (action === 'open-detail') {
+        state.currentPlanId = t.dataset.id
+        state.deliveryTabIndex = 0
+        closeSheet()
+        showScreen('detail')
+      }
+      if (action === 'open-form') {
+        if (t.dataset.id) state.currentPlanId = t.dataset.id
+        state.formMode = t.dataset.mode || 'edit'
+        showScreen('form')
+      }
+      if (action === 'confirm') {
+        if (state.listMorePlanId) state.currentPlanId = state.listMorePlanId
+        openConfirm(t.dataset.kind)
+      }
+      if (action === 'add-stop') addStop(t.dataset.side)
+      if (action === 'add-product') {
+        openPickerSheet('sheet-product', 'product-options', products, 'pick-load-product')
+      }
+      if (action === 'remove-product') {
+        const card = t.closest('.product-card')
+        if (card) {
+          card.remove()
+          toast('已删除产品')
+        }
+      }
+      if (action === 'remove-stop') {
+        const card = t.closest('.stop-card')
+        if (card) {
+          card.remove()
+          toast('已删除该段')
+        }
+      }
+      if (action === 'form-draft') {
+        toast('已暂存(模拟)')
+        showScreen('list', { push: false })
+        state.history = []
+      }
+      if (action === 'form-save') {
+        const label = state.formMode === 'new' || state.formMode === 'copy' ? '已创建(模拟)' : '已保存(模拟)'
+        toast(label)
+        showScreen('detail', { push: false })
+      }
+      if (action === 'form-save-arrange') {
+        const label =
+          state.formMode === 'new' || state.formMode === 'copy'
+            ? '已创建并安排运输(模拟)'
+            : '已保存并安排(模拟)'
+        toast(label)
+        const p = currentPlan()
+        p.status = '已安排'
+        p.tag = 'tag-ok'
+        p.followStatus = 'arranged'
+        p.transportStatus = 'arranged'
+        p.canArrange = true
+        p.canWait = false
+        p.canCancelWait = false
+        p.arrangeLabel = '调整运输'
+        if ($('#f-plate').textContent.trim()) p.plate = $('#f-plate').textContent.trim()
+        showScreen('detail', { push: false })
+      }
+      if (action === 'form-rearrange') {
+        toast('运输调整成功(模拟)')
+        showScreen('detail', { push: false })
+      }
+      if (action === 'pick-vehicle') {
+        $('#f-plate').textContent = t.dataset.label
+        closeSheet()
+        refreshVehicleSummary()
+        toast('已选择车辆')
+      }
+      if (action === 'pick-personnel') {
+        const target = state.personnelRole === '押运' ? '#f-escort' : '#f-driver'
+        $(target).textContent = t.dataset.label
+        closeSheet()
+        refreshVehicleSummary()
+        toast('已选择' + state.personnelRole)
+      }
+      if (action === 'pick-customer') {
+        if (state.pickerTarget) {
+          const el = document.getElementById(state.pickerTarget)
+          if (el) el.textContent = t.dataset.label
+        }
+        closeSheet()
+        toast('已选择客户')
+      }
+      if (action === 'pick-product') {
+        if (state.pickerTarget) {
+          const el = document.getElementById(state.pickerTarget)
+          if (el) el.textContent = t.dataset.label
+        }
+        closeSheet()
+        toast('已选择产品')
+      }
+      if (action === 'pick-load-product') {
+        const name = (t.dataset.label || '').split('·')[0].trim() || t.dataset.label
+        addLoadProduct(name)
+        closeSheet()
+        toast('已添加装货产品')
+      }
+      if (action === 'pick-contract') {
+        if (state.pickerTarget) {
+          const el = document.getElementById(state.pickerTarget)
+          if (el) el.textContent = t.dataset.label
+        }
+        closeSheet()
+        toast('已选择合同')
+      }
+      if (action === 'pick-address') {
+        if (state.pickerTarget) {
+          const el = document.getElementById(state.pickerTarget)
+          if (el) {
+            if (el.tagName === 'INPUT') el.value = t.dataset.label
+            else el.textContent = t.dataset.label
+          }
+        }
+        closeSheet()
+        toast('已从地址库填入')
+      }
+      if (action === 'pick-print-customer') {
+        state.printCustomer = t.dataset.label
+        closeSheet()
+        renderPrint()
+        toast('已选择送货客户')
+      }
+      if (action === 'pick-carrier') {
+        const target = state.pickerTarget || 'paste-carrier'
+        const el = document.getElementById(target) || $('#paste-carrier')
+        if (el) el.textContent = t.dataset.label
+        closeSheet()
+        refreshVehicleSummary()
+        toast('已选择承运商')
+      }
+      if (action === 'paste-parse') {
+        $('#paste-plate').value = '皖HD1234'
+        $('#paste-trailer').value = '皖HG88挂'
+        $('#paste-load').value = '34'
+        $('#paste-driver-name').value = '张伟'
+        $('#paste-driver-phone').value = '13800001111'
+        $('#paste-driver-id').value = '340102199001011234'
+        $('#paste-escort-name').value = '李强'
+        $('#paste-escort-phone').value = '13900002222'
+        $('#paste-escort-id').value = '340102199002021234'
+        $('#paste-carrier').innerHTML = '<span class="ph">请选择承运商</span>'
+        $('#paste-result-block').classList.remove('hidden')
+        $('#paste-submit-btn').disabled = false
+        state.pasteParsed = true
+        const warn = $('#paste-warn')
+        warn.style.display = 'block'
+        warn.textContent = '演示:识别完成,请核对并补全承运商;若有冲突请选择处理方式。'
+        toast('识别完成')
+        setTimeout(() => {
+          $('#paste-result-block').scrollIntoView({ behavior: 'smooth', block: 'start' })
+        }, 50)
+      }
+      if (action === 'pick-conflict') {
+        state.pasteConflict = t.dataset.val
+        $$('.paste-conflict-opt').forEach((o) => o.classList.toggle('active', o === t))
+        toast('冲突处理:' + ({ reuse: '复用', overwrite: '覆盖更新', cancel: '取消' }[t.dataset.val] || t.dataset.val))
+      }
+      if (action === 'paste-submit') {
+        if (!state.pasteParsed) {
+          toast('请先识别')
+          return
+        }
+        if (!$('#paste-trailer').value.trim()) {
+          toast('挂车号必填')
+          return
+        }
+        if (state.pasteConflict === 'cancel') {
+          toast('已按冲突策略取消本条')
+          return
+        }
+        toast('已生成三方人车(模拟)')
+        showScreen('list', { push: false })
+        state.history = []
+      }
+    })
+
+    $('#sheet-mask').addEventListener('click', closeSheet)
+    $('#confirm-ok').addEventListener('click', () => {
+      const p = currentPlan()
+      if (state.confirmKind === 'wait') {
+        p.status = '等通知'
+        p.tag = 'tag-wait'
+        p.canWait = false
+        p.canCancelWait = true
+        toast('已标记为等客户通知')
+      } else if (state.confirmKind === 'cancel-wait') {
+        p.status = '待安排'
+        p.tag = 'tag-warn'
+        p.canWait = true
+        p.canCancelWait = false
+        toast('已取消等通知,恢复为待安排')
+      } else if (state.confirmKind === 'void') {
+        p.status = '已作废'
+        p.tag = 'tag-mute'
+        p.canArrange = false
+        p.canEdit = false
+        p.canVoid = false
+        p.canWait = false
+        p.canCancelWait = false
+        toast('作废成功')
+      }
+      closeSheet()
+      state.listMorePlanId = ''
+      if (state.screen === 'detail') renderDetail()
+      else renderList()
+    })
+
+    $('#list-keyword').addEventListener('input', () => {
+      state.listPage = 1
+      renderList()
+    })
+
+    $$('.sheet-search-input').forEach((input) => {
+      input.addEventListener('input', () => {
+        const sheet = input.closest('.sheet')
+        if (!sheet || sheet.id !== state.picker.sheetId) return
+        state.picker.keyword = input.value || ''
+        renderPickerOptions()
+      })
+      input.addEventListener('click', (e) => e.stopPropagation())
+    })
+
+    renderList()
+  </script>
+</body>
+</html>

+ 173 - 0
docs/superpowers/specs/2026-09-15-mp-webview-pl-plan-mobile-redesign-design.md

@@ -0,0 +1,173 @@
+# 小程序 WebView 运输计划 · 移动端交互重设计 — 设计文档
+
+> **日期**: 2026-09-15  
+> **状态**: 待实现(Demo 已验收)  
+> **前置架构**: `docs/superpowers/specs/2026-09-14-mp-webview-transport-plan-design.md`(壳页、ticket、权限、路由骨架已落地)  
+> **权威 Demo**: `bak/mobile-ui/shanxia-mp-pl-plan-mobile-redesign-demo.html`  
+> **视觉规范**: `docs/miniprogram-ui-spec-sanqiang.md`(三墙 Token;WebView 内 H5 使用 `#0a9f6d` 链,不套 Fresh Mint)
+
+## 1. 目标
+
+现网 `/#/m/pl/plan*` 已能跑通业务,但多为桌面弹窗/`variant=page` 硬套窄屏,信息过密、选择器不适配手指。
+
+本期在 **不改后端业务规则、不改小程序壳与 ticket** 的前提下,按已验收 Demo **重写移动端 UI/交互**,使列表 → 详情 → 表单(含安排)→ 打印 → 三方人车 全链路达到与 Web 管理端同等能力、手机可用。
+
+成功标准:
+
+1. 主路径可在真机 WebView 走通(新增/暂存/创建/安排/调整、复制、等通知、作废、打印预览存图、三方人车)。
+2. 交互与信息架构以 Demo 为准;业务校验与按钮显隐与桌面 `PlPlanFormDialog` / 列表操作一致。
+3. 只发 `web-admin` 静态资源即可更新体验,不必提审小程序。
+
+## 2. 已确认决策
+
+| # | 项 | 决策 |
+|---|-----|------|
+| D1 | 交付路径 | **方案 1**:先 HTML SPA Demo 验收,再按 Demo 重写 `/#/m/pl/*`(已完成 Demo) |
+| D2 | 范围 | 全链路:列表、详情、表单、安排、打印、三方人车快速创建 |
+| D3 | 表单布局 | **长页滚动 + 分区锚点**(基础 / 运输 / 装货 / 送货 / 备注) |
+| D4 | 选择器 / 确认 | **Bottom Sheet**(车辆、人员、客户、产品、承运商、合同、地址库、更多操作、危险确认) |
+| D5 | 安排 vs 编辑 | 与 Web 相同:同一表单,`openForArrange` / 安排模式;底栏按钮随模式变化,不做第二套安排页 |
+| D6 | 架构边界 | 沿用 2026-09-14 文档:小程序通用 WebView 壳 + ticket;本期 **只改 H5 UI** |
+| D7 | 列表筛选 | 对齐 Web `PL_PLAN_LIST_TABS`(含「全部」在草稿中之后);日期快捷 + **自定义区间**;京东式双下拉,非一排 Chip 硬挤 |
+| D8 | 列表新增入口 | 右下角 **Speed Dial**(新增计划 / 三方人车快速创建),不要底栏双主按钮 |
+| D9 | 详情底栏 | 「⋯ 更多」+ 最多 **2** 个主操作(优先编辑、安排/调整);复制/等通知/打印/作废进更多 |
+| D10 | 危化证 | 详情按产品展示证书槽位(易制毒:购买证+运输证;易制爆:合法证明);上传/齐备确认仍在「危化品证上传」页,不塞进计划表单 |
+| D11 | 运费 | 详情只读展示;**编辑态不在本期**(Web 主编辑在归档弹窗,非计划表单) |
+| D12 | 打印 | WebView 内预览 + 引导长按/存图;送(提)货单须先选送货客户 |
+| D13 | 非本期 | 列设置、导出、强制改状态;独立危化证上传移动端重做(可后续单独立项) |
+
+## 3. 非目标
+
+- 不改运输计划后端状态机、租户隔离、数据权限、合同可安排量等业务规则。
+- 不改小程序 `mp-webview` 壳、ticket/exchange、菜单 perms(除非发现缺口再开小变更)。
+- 不下线现场原生「运输计划单」或公开司机/现场链接。
+- 不把桌面 `el-dialog` 作为 WebView 主交互形态保留。
+- 本期不做运费编辑、不做危化证上传页移动端重设计。
+
+## 4. 与前置文档关系
+
+| 文档 | 关系 |
+|------|------|
+| `2026-09-14-mp-webview-transport-plan-design.md` | **架构仍有效**:ticket、壳、权限、路由前缀 `/#/m/pl/plan*` |
+| 本文 | **替换**该文档 §7 的 UI 形态说明;权威 Demo 改为 `shanxia-mp-pl-plan-mobile-redesign-demo.html` |
+| 现网 `web-admin/src/views/m/pl/plan/*` | 实现基线,按本文与 Demo **重写页面结构与样式**,优先复用 API / composable / 表单子逻辑 |
+
+## 5. 信息架构
+
+```text
+列表 /m/pl/plan
+  ├─ Speed Dial → 新增 /m/pl/plan/new
+  ├─ Speed Dial → 三方人车 /m/pl/plan/paste
+  ├─ 卡片 → 详情 /m/pl/plan/:id
+  └─ 卡片 复制 / 安排 → 表单(copy / arrange)
+
+详情
+  ├─ 主操作 → 编辑 / 安排(同表单不同 mode)
+  ├─ 更多 → 复制 / 等通知·取消等通知 / 打印 / 作废
+  └─ 车辆「查看」→ Bottom Sheet 只读详情
+
+表单(new | edit | arrange | copy)
+  └─ 锚点分区;选择器均为 Sheet;底栏对齐 Web 显隐规则
+
+打印 /m/pl/plan/:id/print
+三方人车 /m/pl/plan/paste
+```
+
+路由可继续沿用现有 `/#/m/pl/plan*` 路径;**安排不必独立路由**(与 Web 一致用表单 mode)。若现有 `/arrange` 路由已存在,可保留为打开同一表单组件的入口,避免双实现。
+
+## 6. 分屏规格(对齐 Demo)
+
+### 6.1 列表
+
+- Header:返回工作台、标题「运输计划」、搜索(计划编号/供方/需方/产品/车牌号)。
+- 筛选条:日期下拉(今天/明天/本周/本月/自定义)+ 状态下拉(对齐 `PL_PLAN_LIST_TABS` 顺序)。
+- 卡片:编号、类型·日期、状态 Tag、**供方→需方**、跟进人、车牌、人员、产品摘要。
+- 卡片操作:更多(等通知/作废等)· 复制 · 安排/调整(权限与状态同 Web)。
+- 分页:加载更多。
+- Speed Dial:主色「新增计划」+「三方人车快速创建」。
+
+### 6.2 详情
+
+分区顺序对齐 Web `PlPlanViewTabs`:
+
+1. 基础信息  
+2. 运输信息(已安排:司机链接、**多现场链接**、车辆摘要+查看、司机/现场回传摘要)  
+3. 装送货(装货明细;送货 **多段 Tab**;产品行含型号规格/单位/包装/计划·实际量/送货方式等;数量平衡)  
+4. 危化品证书(按产品 + 槽位附件/未上传 + 齐备状态)  
+5. 运费信息(只读字段集与 Web 一致)  
+6. 备注  
+
+底栏:见 D9。
+
+### 6.3 表单
+
+- 锚点:基础 / 运输 / 装货 / 送货 / 备注。  
+- 运输:车辆库、三方人车入口、司机/押运、承运商、车辆摘要。  
+- 装/送:客户、合同、时间、联系、地址(**地址库 Sheet**)、注意事项、地磅、产品行(含型号规格/品牌/别名/可安排量/数量/单位/包装;送货另有送货方式);送货多段 + 同步装货产品 + 数量平衡。  
+- 底栏(对齐 `PlPlanFormDialog`):  
+  - 新增/复制:取消 / 暂存 / 创建 / 创建并安排运输  
+  - 编辑(待安排/草稿):取消 / 暂存 / 保存 / 保存并安排  
+  - 已安排再安排:取消 / 调整运输  
+- 安排前危化证未齐备:沿用 Web 拦截文案,引导「危化品证上传」(本期可不做该页移动端,可用 toast/外链说明)。
+
+### 6.4 打印
+
+- 单据类型 Chip 与 Web 一致(入库/送提货/分装及空白单)。  
+- 送(提)货单:必选送货客户后再预览。  
+- 主操作:返回详情、保存预览(模拟/实现为引导存图)。
+
+### 6.5 三方人车
+
+字段对齐 `PlPasteRecognizeDialog`(`vehicle-bundle`):原文识别、承运商、主挂车、核载、驾驶员三要素、押运员三要素、冲突处理(复用/覆盖/取消)、确认生成。承运商用 Sheet。
+
+## 7. 组件与样式约定
+
+| 单元 | 约定 |
+|------|------|
+| Token | 三墙:`--brand #0a9f6d` 等;禁止 WebView 页用 `#10b981` 作主色 |
+| Sheet | 遮罩无 backdrop blur;点遮罩关闭;列表类 Sheet 顶部模糊搜索 |
+| 底栏 | 贴底 flex,预留 `safe-area-inset-bottom`;勿用悬浮留白断层 |
+| 状态 Tag | 语义色与 Web/小程序一致(待安排警告、已安排成功、等通知等待、草稿中性) |
+| 复用 | 优先复用 `planApi`、`usePlFreightCalc`(详情只读)、危化槽位工具 `getRequiredCertSlots`、装送货 stop form utils;UI 层按移动端重写,避免再包一层 `el-dialog` |
+
+## 8. 实现落点(供后续计划拆任务)
+
+| 层 | 路径 |
+|----|------|
+| Demo(已完成) | `bak/mobile-ui/shanxia-mp-pl-plan-mobile-redesign-demo.html` |
+| H5 页面 | `web-admin/src/views/m/pl/plan/{index,detail,form,print,paste}.vue` |
+| 壳 / 守卫 | 原则上不动 `MpWebviewShell`、ticket;仅当样式冲突时做最小调整 |
+| 共享逻辑 | 现有 `@/api/pl`、`@/composables/usePl*`、`@/utils/pl-*`、表单子组件按需抽「无 dialog」用法 |
+| 样式 | 页内 SCSS 或 `web-admin/src/styles` 下 mp-webview 专用片段,对齐三墙 Token |
+
+建议实现顺序:
+
+1. 列表(筛选 + 卡片 + Speed Dial + 加载更多)  
+2. 详情(分区 + 危化证槽位 + 底栏更多)  
+3. 表单(锚点 + Sheet 选择器 + 底栏模式)  
+4. 打印 / 三方人车  
+5. 真机 WebView 冒烟与权限回归  
+
+## 9. 错误与边界
+
+| 情况 | 行为 |
+|------|------|
+| 无 `pl:plan:*` | 页内无权限空态(沿用现网) |
+| 安排时危化证未齐 | 拦截提示,不进入成功安排 |
+| Sheet 无匹配 | 「无匹配结果」空态 |
+| 打印未选送货客户 | Toast,不生成预览 |
+| 网络/401 | 沿用 2026-09-14 文档(清 token → 小程序登录) |
+
+## 10. 测试要点
+
+- Demo 对照:列表双下拉、Speed Dial、详情多送货段与危化槽位、表单锚点与四按钮新增、打印选客户、三方人车全字段。  
+- 与桌面同账号:列表数据范围、按钮显隐(草稿/待安排/已安排/等通知)、创建/安排/调整结果一致。  
+- 窄屏 375:底栏不遮挡、Sheet 可滚、锚点可跳。  
+- 仅发 H5 静态资源后小程序内可见变更。  
+- 现场「运输计划单」原生页不受影响。
+
+## 11. 风险
+
+- 桌面表单逻辑重,移动端易漏字段或漏校验:以 Demo 字段清单 + `PlPlanFormDialog` 提交 payload 为核对表。  
+- 现网 `/m/pl/*` 已上线,重写时建议按屏替换并保留 API 层,避免中断 ticket 壳。  
+- WebView 文件预览弱于桌面:危化附件以「已上传文件名 + 可点预览/打开」为最低交付,不强求与桌面上传组件像素一致。