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