From 121e431f68b516eba0aac48c1f0ce8e8f68643ab Mon Sep 17 00:00:00 2001 From: wuzilong <704541015@qq.com> Date: Thu, 15 Oct 2026 15:43:16 +0800 Subject: [PATCH] docs: add mp-webview CRM quote implementation plan. Break the quote manage/approve H5 migration into TDD-friendly tasks aligned with the product webview pattern. Co-authored-by: Cursor --- .../plans/2026-10-15-mp-webview-crm-quote.md | 942 ++++++++++++++++++ 1 file changed, 942 insertions(+) create mode 100644 docs/superpowers/plans/2026-10-15-mp-webview-crm-quote.md diff --git a/docs/superpowers/plans/2026-10-15-mp-webview-crm-quote.md b/docs/superpowers/plans/2026-10-15-mp-webview-crm-quote.md new file mode 100644 index 000000000..455484bb8 --- /dev/null +++ b/docs/superpowers/plans/2026-10-15-mp-webview-crm-quote.md @@ -0,0 +1,942 @@ +# 小程序 WebView · CRM 报价单 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:** 在小程序工作台开放「CRM管理 → 报价单管理 / 报价单审批」,通过既有 `mp-webview` 壳打开 `web-admin` `/#/m/crm/quote*`,提供管理闭环(列表/详情/增改删/单条提交审批/复制新增/作废/版本历史只读)与审批闭环(待审/已审列表 + 单条通过驳回),不含分享、转合同、富文本/PDF、导出与批量。 + +**Architecture:** 复用运输计划 / CRM 产品 WebView 模板(ticket → exchange → `MpWebviewShell`)。新建移动端报价页,样式对齐 `/#/m/pl/plan*` 与 `/#/m/crm/product*`(`mp-webview.scss` 三墙 Token)。业务 API 全部复用 `@/api/crm/quote`。桌面 `QuoteFormDialog`/`QuoteFormPanel` 增加 `variant="page"`;列表抽共享组件 + `mode`。菜单 SQL 幂等写入 `MINI_CRM_QUOTE*`,按 Web `crm:quote:list` / `crm:quote:approve` 角色分别补绑。 + +**Tech Stack:** Vue 3 · Vue Router hash · Element Plus · Pinia · uni-app web-view · MySQL `sys_menu`/`sys_role_menu` · Node test(`pnpm exec tsx --test`) + +**Spec:** `docs/superpowers/specs/2026-10-15-mp-webview-crm-quote-design.md` + +--- + +## 文件结构 + +| 路径 | 职责 | +|------|------| +| `web-admin/src/utils/mp-crm-quote-card.ts` | 列表 Tab/查询参数、卡片字段、金额格式(可单测) | +| `web-admin/src/utils/mp-crm-quote-card.test.ts` | 纯函数单测 | +| `web-admin/src/utils/mp-webview.ts` | 增加 `MP_CRM_QUOTE_LIST_PATH`、`MP_CRM_QUOTE_APPROVAL_PATH` | +| `web-admin/src/views/m/crm/quote/MpQuoteList.vue` | 共享列表(mode=manage\|approve) | +| `web-admin/src/views/m/crm/quote/index.vue` | 管理列表壳 | +| `web-admin/src/views/m/crm/quote/approval.vue` | 审批列表壳 | +| `web-admin/src/views/m/crm/quote/detail.vue` | 详情 + 管理/审批底栏 | +| `web-admin/src/views/m/crm/quote/form.vue` | 路由壳,挂载 `QuoteFormDialog variant="page"` | +| `web-admin/src/views/m/crm/quote/versions.vue` | 版本历史只读 | +| `web-admin/src/views/crm/quote/components/QuoteFormDialog.vue` | 增加 `variant: 'dialog' \| 'page'` | +| `web-admin/src/views/crm/quote/components/QuoteFormPanel.vue` | page 模式单列布局(若 dialog 透传不够再改) | +| `web-admin/src/router/routes/staticRoutes.ts` | 注册 `/m/crm/quote*`(catch-all 前) | +| `miniprogram/config/workbench-menus.js` | 两项 enabled + code + perms | +| `miniprogram/config/api.js` | `WORKBENCH_ROUTE_MAP` 两项 | +| `docs/sql/migrations/2026-10-15-mini-crm-quote-webview-menus.sql` | 菜单 + 角色补绑 | +| `docs/web-admin-mp-webview-modules.md` | 补充报价对照行 | + +**明确不改:** `mp-webview.vue` 鉴权协议、后端报价业务 API 语义、桌面 `/#/crm/quote*` 默认行为(仅表单组件增加可选 `variant`,默认仍为 `dialog`)。 + +**查询语义(重要,与桌面审批 Tab 不同):** + +| mode | view | 请求要点 | +|------|------|----------| +| manage | `all` | `excludeStatus=invalid`;不传 status / pending / approved | +| manage | `submitted` | `status=submitted`;**不**传 `pendingApprovalUserId` | +| manage | `accepted` | `status=accepted`;**不**传 `approvedByUserId` | +| approve | `submitted` | `status=submitted` + `pendingApprovalUserId=当前用户` | +| approve | `accepted` | `approvedByUserId=当前用户`(与桌面审批「已审批」一致) | + +分页字段用 `pageNum` / `pageSize`(对齐 `QuoteQuery`,不是产品的 `page`)。 + +--- + +### Task 1: 列表卡片纯函数 + 单测 + +**Files:** +- Create: `web-admin/src/utils/mp-crm-quote-card.ts` +- Create: `web-admin/src/utils/mp-crm-quote-card.test.ts` + +- [ ] **Step 1: 写失败单测** + +```ts +// web-admin/src/utils/mp-crm-quote-card.test.ts +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' +import { + MP_QUOTE_MANAGE_TABS, + MP_QUOTE_APPROVE_TABS, + buildQuoteListQuery, + buildQuoteCard, + formatQuoteMoney, + quoteTypeLabel, + quoteTypeTagType +} from './mp-crm-quote-card' + +describe('tabs', () => { + it('manage has all/submitted/accepted; approve has submitted/accepted', () => { + assert.deepEqual( + MP_QUOTE_MANAGE_TABS.map((x) => x.value), + ['all', 'submitted', 'accepted'] + ) + assert.deepEqual( + MP_QUOTE_APPROVE_TABS.map((x) => x.value), + ['submitted', 'accepted'] + ) + }) +}) + +describe('buildQuoteListQuery', () => { + it('manage all excludes invalid and trims keyword', () => { + assert.deepEqual( + buildQuoteListQuery({ + mode: 'manage', + view: 'all', + pageNum: 1, + pageSize: 20, + keyword: ' 酸 ', + currentUserId: 'u1' + }), + { + pageNum: 1, + pageSize: 20, + keyword: '酸', + excludeStatus: 'invalid' + } + ) + }) + + it('manage submitted sets status only', () => { + const q = buildQuoteListQuery({ + mode: 'manage', + view: 'submitted', + pageNum: 2, + pageSize: 10, + keyword: '', + currentUserId: 'u1' + }) + assert.equal(q.status, 'submitted') + assert.equal(q.excludeStatus, undefined) + assert.equal('pendingApprovalUserId' in q, false) + assert.equal('keyword' in q, false) + }) + + it('manage accepted sets status accepted', () => { + const q = buildQuoteListQuery({ + mode: 'manage', + view: 'accepted', + pageNum: 1, + pageSize: 20, + keyword: '', + currentUserId: 'u1' + }) + assert.equal(q.status, 'accepted') + assert.equal('approvedByUserId' in q, false) + }) + + it('approve submitted uses pendingApprovalUserId', () => { + const q = buildQuoteListQuery({ + mode: 'approve', + view: 'submitted', + pageNum: 1, + pageSize: 20, + keyword: '', + currentUserId: 'u9' + }) + assert.equal(q.status, 'submitted') + assert.equal(q.pendingApprovalUserId, 'u9') + }) + + it('approve accepted uses approvedByUserId', () => { + const q = buildQuoteListQuery({ + mode: 'approve', + view: 'accepted', + pageNum: 1, + pageSize: 20, + keyword: 'x', + currentUserId: 'u9' + }) + assert.equal(q.approvedByUserId, 'u9') + assert.equal(q.keyword, 'x') + assert.equal('status' in q && q.status !== undefined, false) + }) +}) + +describe('buildQuoteCard', () => { + it('maps fields and money', () => { + const card = buildQuoteCard({ + id: 'q1', + quoteNo: 'BJ001', + quoteTitle: '硫酸报价', + customerName: '甲公司', + quoteType: 'sales', + finalAmount: 1234.5, + status: 'draft', + quoteDate: '2026-10-01' + }) + assert.equal(card.id, 'q1') + assert.equal(card.quoteNo, 'BJ001') + assert.equal(card.quoteTypeLabel, '销售') + assert.equal(card.quoteTypeTagType, 'success') + assert.equal(card.amountText, '¥1,234.50') + assert.equal(card.status, 'draft') + }) +}) + +describe('quoteType helpers', () => { + it('maps purchase/sales', () => { + assert.equal(quoteTypeLabel('purchase'), '采购') + assert.equal(quoteTypeTagType('purchase'), 'warning') + assert.equal(quoteTypeLabel('sales'), '销售') + assert.equal(formatQuoteMoney(1000), '¥1,000.00') + assert.equal(formatQuoteMoney(undefined), '—') + }) +}) +``` + +- [ ] **Step 2: 跑测确认失败** + +```bash +cd web-admin && pnpm exec tsx --test src/utils/mp-crm-quote-card.test.ts +``` + +Expected: 无法解析 `./mp-crm-quote-card` 或 FAIL + +- [ ] **Step 3: 实现纯函数** + +```ts +// web-admin/src/utils/mp-crm-quote-card.ts +export type MpQuoteMode = 'manage' | 'approve' +export type MpQuoteView = 'all' | 'submitted' | 'accepted' + +export const MP_QUOTE_MANAGE_TABS: Array<{ value: MpQuoteView; label: string }> = [ + { value: 'all', label: '全部' }, + { value: 'submitted', label: '待审批' }, + { value: 'accepted', label: '已审批' } +] + +export const MP_QUOTE_APPROVE_TABS: Array<{ value: Exclude; label: string }> = [ + { value: 'submitted', label: '待审批' }, + { value: 'accepted', label: '已审批' } +] + +export function quoteTypeLabel(type?: string): string { + if ((type || '').trim() === 'purchase') return '采购' + return '销售' +} + +export function quoteTypeTagType(type?: string): 'success' | 'warning' { + if ((type || '').trim() === 'purchase') return 'warning' + return 'success' +} + +export function formatQuoteMoney(amount?: number | null): string { + if (amount === undefined || amount === null || Number.isNaN(Number(amount))) return '—' + return `¥${Number(amount).toLocaleString('zh-CN', { + minimumFractionDigits: 2, + maximumFractionDigits: 2 + })}` +} + +export function buildQuoteListQuery(input: { + mode: MpQuoteMode + view: MpQuoteView + pageNum: number + pageSize: number + keyword: string + currentUserId: string +}): Record { + const params: Record = { + pageNum: input.pageNum, + pageSize: input.pageSize + } + const kw = String(input.keyword || '').trim() + if (kw) params.keyword = kw + + if (input.mode === 'manage') { + if (input.view === 'all') { + params.excludeStatus = 'invalid' + } else if (input.view === 'submitted') { + params.status = 'submitted' + } else { + params.status = 'accepted' + } + return params + } + + // approve + if (input.view === 'submitted') { + params.status = 'submitted' + params.pendingApprovalUserId = input.currentUserId + } else { + params.approvedByUserId = input.currentUserId + } + return params +} + +export function buildQuoteCard(row: { + id: string | number + quoteNo?: string + quoteTitle?: string + customerName?: string + quoteType?: string + finalAmount?: number + totalAmount?: number + status?: string + quoteDate?: string +}) { + const amount = + row.finalAmount !== undefined && row.finalAmount !== null + ? row.finalAmount + : row.totalAmount + return { + id: String(row.id), + quoteNo: row.quoteNo || '—', + quoteTitle: row.quoteTitle || '—', + customerName: row.customerName || '—', + quoteType: row.quoteType || 'sales', + quoteTypeLabel: quoteTypeLabel(row.quoteType), + quoteTypeTagType: quoteTypeTagType(row.quoteType), + amountText: formatQuoteMoney(amount), + status: row.status || '', + quoteDate: row.quoteDate || '—' + } +} +``` + +状态 Tag 文案/类型在页面用 `dictStore.getLabel/getTagType('crm_quote_status', status)`(与桌面一致);纯函数只保留 `status` 原值。 + +- [ ] **Step 4: 跑测确认通过** + +```bash +cd web-admin && pnpm exec tsx --test src/utils/mp-crm-quote-card.test.ts +``` + +Expected: 全部 pass + +- [ ] **Step 5: Commit** + +```bash +git add web-admin/src/utils/mp-crm-quote-card.ts web-admin/src/utils/mp-crm-quote-card.test.ts +git commit -m "$(cat <<'EOF' +feat(web-admin): add mp CRM quote card query helpers. + +EOF +)" +``` + +--- + +### Task 2: 菜单 SQL(报价两叶子 + 角色补绑) + +**Files:** +- Create: `docs/sql/migrations/2026-10-15-mini-crm-quote-webview-menus.sql` + +- [ ] **Step 1: 写入完整幂等 SQL** + +```sql +-- 小程序:CRM管理 → 报价单管理 / 报价单审批(WebView) +-- list 随 CRM3000000000000000000000000010 (crm:quote:list) +-- approve 随 CRM3000000000000000000000000016 (crm:quote:approve) +SET NAMES utf8mb4; + +INSERT INTO `sys_menu` ( + `id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`, + `is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`, + `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag` +) +SELECT + 'MINI_CRM_ROOT', 'CRM管理', '1', 'M', '/mini/crm', NULL, NULL, + 1, '1', '1', 'shanxia2.0', 2, '小程序CRM管理', + 'system', '', NOW(), NULL, NOW(), 0 +FROM DUAL +WHERE NOT EXISTS ( + SELECT 1 FROM `sys_menu` WHERE `id` = 'MINI_CRM_ROOT' AND `del_flag` = 0 +); + +INSERT INTO `sys_menu` ( + `id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`, + `is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`, + `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag` +) +SELECT + 'MINI_CRM_QUOTE', '报价单管理', 'MINI_CRM_ROOT', 'C', 'mp-webview', NULL, + 'mini:crm:quote:list', + 1, '1', '1', 'shanxia2.0', 2, '小程序 WebView CRM 报价单管理', + 'system', '', NOW(), NULL, NOW(), 0 +FROM DUAL +WHERE NOT EXISTS ( + SELECT 1 FROM `sys_menu` WHERE `id` = 'MINI_CRM_QUOTE' AND `del_flag` = 0 +); + +INSERT INTO `sys_menu` ( + `id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`, + `is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`, + `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag` +) +SELECT + 'MINI_CRM_QUOTE_APPROVAL', '报价单审批', 'MINI_CRM_ROOT', 'C', 'mp-webview', NULL, + 'mini:crm:quote:approve', + 1, '1', '1', 'shanxia2.0', 3, '小程序 WebView CRM 报价单审批', + 'system', '', NOW(), NULL, NOW(), 0 +FROM DUAL +WHERE NOT EXISTS ( + SELECT 1 FROM `sys_menu` WHERE `id` = 'MINI_CRM_QUOTE_APPROVAL' AND `del_flag` = 0 +); + +-- 有 Web 报价单管理权 → 补绑根 + CRM目录 + 报价单管理 +INSERT INTO `sys_role_menu` ( + `id`, `role_id`, `menu_id`, `tenant_no`, `order_num`, `remark`, + `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag` +) +SELECT + MD5(CONCAT(r.id, '|', m.menu_id, '|mini-crm-quote-webview|v1')), + r.id, + m.menu_id, + IFNULL(NULLIF(r.tenant_no, ''), 'shanxia2.0'), + 0, + '小程序CRM报价单管理:随 Web crm:quote:list 补绑', + 'system', '', NOW(), NULL, NOW(), 0 +FROM `sys_role` r +JOIN `sys_role_menu` web + ON web.role_id = r.id + AND web.menu_id = 'CRM3000000000000000000000000010' + AND web.del_flag = 0 +JOIN ( + SELECT '1' AS menu_id UNION ALL + SELECT 'MINI_CRM_ROOT' UNION ALL + SELECT 'MINI_CRM_QUOTE' +) m +WHERE r.del_flag = 0 + AND NOT EXISTS ( + SELECT 1 FROM `sys_role_menu` x + WHERE x.role_id = r.id AND x.menu_id = m.menu_id AND x.del_flag = 0 + ); + +-- 有 Web 报价单审批权 → 补绑根 + CRM目录 + 报价单审批 +INSERT INTO `sys_role_menu` ( + `id`, `role_id`, `menu_id`, `tenant_no`, `order_num`, `remark`, + `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag` +) +SELECT + MD5(CONCAT(r.id, '|', m.menu_id, '|mini-crm-quote-approval-webview|v1')), + r.id, + m.menu_id, + IFNULL(NULLIF(r.tenant_no, ''), 'shanxia2.0'), + 0, + '小程序CRM报价单审批:随 Web crm:quote:approve 补绑', + 'system', '', NOW(), NULL, NOW(), 0 +FROM `sys_role` r +JOIN `sys_role_menu` web + ON web.role_id = r.id + AND web.menu_id = 'CRM3000000000000000000000000016' + AND web.del_flag = 0 +JOIN ( + SELECT '1' AS menu_id UNION ALL + SELECT 'MINI_CRM_ROOT' UNION ALL + SELECT 'MINI_CRM_QUOTE_APPROVAL' +) m +WHERE r.del_flag = 0 + AND NOT EXISTS ( + SELECT 1 FROM `sys_role_menu` x + WHERE x.role_id = r.id AND x.menu_id = m.menu_id AND x.del_flag = 0 + ); +``` + +- [ ] **Step 2: Commit(本 Task 只交 SQL;测试库实施时执行)** + +```bash +git add docs/sql/migrations/2026-10-15-mini-crm-quote-webview-menus.sql +git commit -m "$(cat <<'EOF' +docs(sql): add miniprogram CRM quote webview menus and role bind. + +EOF +)" +``` + +--- + +### Task 3: 静态路由 + 路径常量 + +**Files:** +- Modify: `web-admin/src/utils/mp-webview.ts` +- Modify: `web-admin/src/router/routes/staticRoutes.ts` + +- [ ] **Step 1: 增加路径常量** + +在 `MP_CRM_PRODUCT_LIST_PATH` 旁增加: + +```ts +export const MP_CRM_QUOTE_LIST_PATH = '/m/crm/quote' +export const MP_CRM_QUOTE_APPROVAL_PATH = '/m/crm/quote/approval' +``` + +- [ ] **Step 2: 在 catch-all 之前注册路由(顺序固定)** + +插在 `/m/crm/product/:id` 与 `/:pathMatch(.*)*` 之间: + +```ts + { + path: '/m/crm/quote', + name: 'MpCrmQuoteList', + component: () => import('@views/m/crm/quote/index.vue'), + meta: { title: '报价单管理', isHideTab: true, isMpWebview: true } + }, + { + path: '/m/crm/quote/approval', + name: 'MpCrmQuoteApproval', + component: () => import('@views/m/crm/quote/approval.vue'), + meta: { title: '报价单审批', isHideTab: true, isMpWebview: true } + }, + { + path: '/m/crm/quote/new', + name: 'MpCrmQuoteNew', + component: () => import('@views/m/crm/quote/form.vue'), + meta: { title: '新增报价单', isHideTab: true, isMpWebview: true } + }, + { + path: '/m/crm/quote/:id/edit', + name: 'MpCrmQuoteEdit', + component: () => import('@views/m/crm/quote/form.vue'), + meta: { title: '编辑报价单', isHideTab: true, isMpWebview: true } + }, + { + path: '/m/crm/quote/:id/versions', + name: 'MpCrmQuoteVersions', + component: () => import('@views/m/crm/quote/versions.vue'), + meta: { title: '版本历史', isHideTab: true, isMpWebview: true } + }, + { + path: '/m/crm/quote/:id', + name: 'MpCrmQuoteDetail', + component: () => import('@views/m/crm/quote/detail.vue'), + meta: { title: '报价单详情', isHideTab: true, isMpWebview: true } + }, +``` + +本步可先建空壳 vue(仅 `MpWebviewShell` + 标题),后续 Task 填满;**不得**把路由放在 catch-all 之后。 + +- [ ] **Step 3: Commit** + +```bash +git add web-admin/src/utils/mp-webview.ts web-admin/src/router/routes/staticRoutes.ts \ + web-admin/src/views/m/crm/quote/index.vue \ + web-admin/src/views/m/crm/quote/approval.vue \ + web-admin/src/views/m/crm/quote/form.vue \ + web-admin/src/views/m/crm/quote/detail.vue \ + web-admin/src/views/m/crm/quote/versions.vue +git commit -m "$(cat <<'EOF' +feat(web-admin): register /m/crm/quote static routes. + +EOF +)" +``` + +--- + +### Task 4: 共享列表 + 管理/审批入口页 + +**Files:** +- Create: `web-admin/src/views/m/crm/quote/MpQuoteList.vue` +- Modify: `web-admin/src/views/m/crm/quote/index.vue` +- Modify: `web-admin/src/views/m/crm/quote/approval.vue` + +对照:`web-admin/src/views/m/crm/product/index.vue`(Header / filter / card / speed-dial / `goToMiniWorkbench`)。 + +- [ ] **Step 1: 实现 `MpQuoteList.vue`** + +Props:`mode: 'manage' | 'approve'`。 + +关键行为: + +1. `MpWebviewShell @ready` 后再拉列表 +2. 当前用户 id:与桌面 `QuoteListPage` 的 `resolveCurrentUserId` 同源(查 user store;实现时打开该文件复制同一取值,勿猜字段名) +3. `listQuote(buildQuoteListQuery({ mode, view, pageNum, pageSize, keyword, currentUserId }))` +4. 解析 `res.list` / `res.rows` + `total`(与桌面 `getList` 一致) +5. Tab:manage 用 `MP_QUOTE_MANAGE_TABS`(默认 `all`);approve 用 `MP_QUOTE_APPROVE_TABS`(默认 `submitted`);UI 可用 `mp-wv-filter-bar` 或横向 pill,样式对齐产品筛选 +6. 搜索 placeholder:`报价主题 / 客户 / 单号`;enter / 清除后 reload +7. 卡片字段:`buildQuoteCard` + 状态 `el-tag`(`dictStore`) +8. 点击详情:`router.push({ path: \`/m/crm/quote/${id}\`, query: { from: mode } })` +9. manage 才有 Speed Dial「新增报价单」→ `/m/crm/quote/new`;approve **无** Speed Dial +10. 返回:`goToMiniWorkbench()`(与产品列表相同) +11. `import '@/styles/mp-webview.scss'` +12. **不要**出现:多选、导出、facet、统计条、日期快捷、批量、分享、生成报价单 + +卡片模板要点: + +```vue +
+
{{ card.quoteTitle }}
+
{{ card.quoteNo }} · {{ card.customerName }}
+
{{ card.amountText }} · {{ card.quoteDate }}
+
+ {{ card.quoteTypeLabel }} + {{ statusLabel(card.status) }} +
+
+``` + +分页:与产品页一致「加载更多」(`pageNum++`)。 + +403 → `forbidden` 显示「无权限」。 + +- [ ] **Step 2: 薄壳页** + +`index.vue`: + +```vue + + +``` + +`approval.vue`:`mode="approve"`。 + +- [ ] **Step 3: 本地验证** + +```bash +cd web-admin && pnpm dev +``` + +打开 `/#/m/crm/quote`、`/#/m/crm/quote/approval`(桌面登录或 mp 调试),确认 Tab/搜索/卡片。 + +- [ ] **Step 4: Commit** + +```bash +git add web-admin/src/views/m/crm/quote/MpQuoteList.vue \ + web-admin/src/views/m/crm/quote/index.vue \ + web-admin/src/views/m/crm/quote/approval.vue +git commit -m "$(cat <<'EOF' +feat(web-admin): add mp webview CRM quote list pages. + +EOF +)" +``` + +--- + +### Task 5: 详情页(管理动作 + 审批动作) + +**Files:** +- Create/Modify: `web-admin/src/views/m/crm/quote/detail.vue` + +对照:`web-admin/src/views/m/crm/product/detail.vue`;字段与动作对齐 `QuoteDetailDialog.vue` + `crm/quote/detail.vue`(删除用 `canDeleteByCreator`)。 + +- [ ] **Step 1: 实现详情加载与分区展示** + +1. `getQuote(id)`;必要时 `listQuoteProduct` / 详情已带 `productList`;`listApproval(id)` 展示审批流 +2. 分区:基础信息、金额/条款、产品明细(卡片或简易表)、审批流、备注附件(有则展示) +3. `from`:`route.query.from === 'approve' ? 'approve' : 'manage'` +4. 返回列表:`from===approve` → `MP_CRM_QUOTE_APPROVAL_PATH`,否则 `MP_CRM_QUOTE_LIST_PATH` +5. 加载 skeleton 用运输计划/产品同款 class + +- [ ] **Step 2: 管理底栏(`from!==approve`)** + +按 spec §5.4 + `quoteStatus.ts`: + +| 条件 | 按钮 | +|------|------| +| `status==='draft'` | 提交审批(主色)、编辑、删除(`canDeleteByCreator`)、作废、复制新增、版本历史 | +| `isQuoteEditableStatus` 且非 draft | 编辑(先 `getQuoteReeditConfirmMessage` 确认)、作废、复制新增、版本历史 | +| `status==='invalid'` | 复制新增、版本历史 | +| 始终禁止 | 生成报价单、分享、转合同、通过/驳回 | + +动作: + +- 编辑 → `/m/crm/quote/:id/edit` +- 复制新增 → `/m/crm/quote/new?copyFrom=:id` +- 版本历史 → `/m/crm/quote/:id/versions` +- 删除 → 确认后 `delQuote([id])`,成功回管理列表 +- 作废 → 输入原因后 `invalidQuote(id, reason)`,成功刷新 +- 提交审批 → 打开 Bottom Sheet(见 Step 3) + +- [ ] **Step 3: 提交审批 Sheet(单条)** + +1. 打开时 `getQuoteApproverSetting()` 预填 `approverIds`(与桌面 submit 对话框同源字段) +2. 审批人多选(有序:用 `el-select multiple` 或与桌面 `ApproverSelector` 一致的数据源——打开 `QuoteListPage` / `QuoteFormPanel` 看审批人列表从哪来,复用同一 API) +3. 备注可选 +4. 确认 → `submitQuote({ ids: [id], approverIds, remark })`(以 `SubmitApprovalForm` 实际字段为准,打开 `types.ts` 核对,勿猜) +5. 成功关闭 Sheet 并刷新详情 + +- [ ] **Step 4: 审批底栏(`from===approve`)** + +1. 从 `listApproval` 判定当前用户是否有 `pending` 节点;有则显示通过/驳回 +2. 通过/驳回:结果 + 意见(驳回必填);调用 `approveQuote`(字段对齐桌面 `ApprovalForm`) +3. **不出现**编辑/提交/删除/作废/复制 + +- [ ] **Step 5: 手工验证** + +草稿提交 → 审批页可见 → 通过/驳回;管理页无审批按钮。 + +- [ ] **Step 6: Commit** + +```bash +git add web-admin/src/views/m/crm/quote/detail.vue +git commit -m "$(cat <<'EOF' +feat(web-admin): add mp webview CRM quote detail actions. + +EOF +)" +``` + +--- + +### Task 6: QuoteFormDialog `variant="page"` + form 路由壳 + +**Files:** +- Modify: `web-admin/src/views/crm/quote/components/QuoteFormDialog.vue` +- Modify: `web-admin/src/views/crm/quote/components/QuoteFormPanel.vue`(page 布局) +- Create/Modify: `web-admin/src/views/m/crm/quote/form.vue` + +对照:`ProductFormDialog` 的 `variant` / `isPageVariant`;`views/m/crm/product/form.vue`。 + +- [ ] **Step 1: QuoteFormDialog 增加 variant(默认 dialog)** + +```ts +const props = withDefaults( + defineProps<{ + modelValue?: boolean + variant?: 'dialog' | 'page' + /** page 模式:add | edit | copy */ + pageMode?: 'add' | 'edit' | 'copy' + quoteId?: string | number | null + copyFromId?: string | number | null + }>(), + { variant: 'dialog', modelValue: false, pageMode: 'add' } +) +const isPageVariant = computed(() => props.variant === 'page') +``` + +行为: + +- `dialog`:保持现有 `open` / `openEdit` / `openCopy` + `el-dialog`,桌面调用处不传 `variant` +- `page`:不渲染 `el-dialog`;根为 `div.mp-quote-form-page`;内嵌 `QuoteFormPanel`;`onMounted`/`watch` 按 `pageMode` 调 `initNew` / `initEdit` / `initCopy` +- page 底栏:取消(Close)/ 保存草稿(DocumentCopy)/ 提交审批(CircleCheck)——与桌面 footer 三按钮一致,调用 panel 同名方法;或若 panel 自带 footer(`hideFooter=false`)则 page 模式 `hideFooter=false` 并加 `mp-plan-form-footer` 样式覆盖 +- 成功 `@success`、关闭 `@close`;page 模式 `modelValue` 可恒 `true` + +- [ ] **Step 2: QuoteFormPanel page 视觉** + +当 `variant==='page'`(新增 prop,默认 `dialog`): + +- 主表栅格改为单列(覆盖多列 grid) +- 明细行改为移动端卡片编辑(禁止宽桌面表原样横向溢出;可横向滚动作为底线,优先卡片) +- 使用三墙相关间距;**不要**改 dialog 默认布局 + +若改动面过大:允许 page 模式先「单列 + 明细横向滚动」,但字段集合必须与桌面一致(全字段)。 + +- [ ] **Step 3: form.vue 壳** + +```vue + +``` + +- `MpCrmQuoteNew` + `copyFrom` query → `pageMode='copy'`,`copyFromId` +- `MpCrmQuoteNew` 无 copy → `pageMode='add'` +- `MpCrmQuoteEdit` → `pageMode='edit'`,`quoteId=route.params.id` +- 成功:优先进详情(若接口返回 id);否则回 `MP_CRM_QUOTE_LIST_PATH` +- `import '@/styles/mp-webview.scss'` + +- [ ] **Step 4: 验证全字段保存(含明细行)与复制新增** + +- [ ] **Step 5: Commit** + +```bash +git add web-admin/src/views/crm/quote/components/QuoteFormDialog.vue \ + web-admin/src/views/crm/quote/components/QuoteFormPanel.vue \ + web-admin/src/views/m/crm/quote/form.vue +git commit -m "$(cat <<'EOF' +feat(web-admin): add page variant for quote form in mp webview. + +EOF +)" +``` + +--- + +### Task 7: 版本历史只读页 + +**Files:** +- Create/Modify: `web-admin/src/views/m/crm/quote/versions.vue` + +对照:`VersionHistoryDialog.vue` 列表字段;**只做只读**(不做对比/恢复/复制为新版本/下载 PDF)。 + +- [ ] **Step 1: 实现** + +1. 先 `getQuote(id)` 取 `quoteNo`,再 `queryVersions(quoteNo)` +2. 时间线或卡片:版本号、状态 Tag、金额、日期、是否当前版本 +3. 「查看」→ `router.push({ path: \`/m/crm/quote/${version.id}\`, query: { from: 'manage' } })`(只读进详情即可) +4. 返回:`router.push(\`/m/crm/quote/${route.params.id}\`)` 或 `router.back()` +5. **不出现**下拉:对比 / 设为当前 / 复制为新版本 / 下载 PDF + +- [ ] **Step 2: Commit** + +```bash +git add web-admin/src/views/m/crm/quote/versions.vue +git commit -m "$(cat <<'EOF' +feat(web-admin): add mp webview CRM quote version history page. + +EOF +)" +``` + +--- + +### Task 8: 小程序工作台入口 + +**Files:** +- Modify: `miniprogram/config/workbench-menus.js` +- Modify: `miniprogram/config/api.js` + +- [ ] **Step 1: CRM menus 增加两项** + +```js +{ + key: 'crm', + title: 'CRM 管理', + desc: '产品 · 报价', + iconText: 'CRM', + visible: true, + menus: [ + { + name: '产品管理', + enabled: true, + code: 'crm-product-webview', + icon: '📦', + perms: 'mini:crm:product:list' + }, + { + name: '报价单管理', + enabled: true, + code: 'crm-quote-webview', + icon: '📝', + perms: 'mini:crm:quote:list' + }, + { + name: '报价单审批', + enabled: true, + code: 'crm-quote-approval-webview', + icon: '✅', + perms: 'mini:crm:quote:approve' + } + ] +} +``` + +- [ ] **Step 2: WORKBENCH_ROUTE_MAP** + +```js +'crm-product-webview': + PAGE_PATHS.mpWebview + '?path=' + encodeURIComponent('/m/crm/product'), +'crm-quote-webview': + PAGE_PATHS.mpWebview + '?path=' + encodeURIComponent('/m/crm/quote'), +'crm-quote-approval-webview': + PAGE_PATHS.mpWebview + '?path=' + encodeURIComponent('/m/crm/quote/approval') +``` + +禁止新建第二个 web-view 页。 + +- [ ] **Step 3: Commit** + +```bash +git add miniprogram/config/workbench-menus.js miniprogram/config/api.js +git commit -m "$(cat <<'EOF' +feat(miniprogram): enable CRM quote webview workbench entries. + +EOF +)" +``` + +--- + +### Task 9: 接入文档 + 回归清单 + +**Files:** +- Modify: `docs/web-admin-mp-webview-modules.md` + +- [ ] **Step 1: 在「CRM 产品对照」后增加报价对照** + +```markdown +## CRM 报价对照 + +- 管理列表 `/m/crm/quote`,审批列表 `/m/crm/quote/approval`,详情 `/:id`,表单 `/new` `/:id/edit`,版本 `/:id/versions` +- 工作台 code `crm-quote-webview` / `crm-quote-approval-webview`,perms `mini:crm:quote:list` / `mini:crm:quote:approve` +- 业务 API:现有报价 list/detail/CRUD/submit/invalid/versions/approval;审批人设置仅 get 预填 +- 不做:分享、转合同、生成报价单/PDF、导出、批量提交/审批、审批人设置菜单 +``` + +并将文首「已落地」句补上报价。 + +- [ ] **Step 2: 回归(对照 spec §10)** + +在测试库执行 Task 2 SQL 后: + +1. 有 `crm:quote:list` 的角色:工作台见「报价单管理」;有 `crm:quote:approve` 见「报价单审批」 +2. 管理列表 Tab/搜索与桌面同账号数据合理一致;审批列表待审默认 +3. 新增草稿 → 详情提交审批(选手+备注)→ 审批入口单条通过/驳回 +4. 管理页无审批按钮;审批页无编辑/作废/提交 +5. 复制新增、作废、版本历史只读;再编辑确认文案 +6. 全字段表单(含明细)保存回显 +7. 401 回小程序;移动端无分享/转合同/PDF/导出/批量 +8. 桌面 `/#/crm/quote`、`/#/crm/quote/approval` 仍为 dialog 形态正常 + +- [ ] **Step 3: Commit** + +```bash +git add docs/web-admin-mp-webview-modules.md +git commit -m "$(cat <<'EOF' +docs: document CRM quote as mp webview module. + +EOF +)" +``` + +--- + +## Spec 覆盖自检 + +| Spec 项 | Task | +|---------|------| +| 嵌套 H5 / 复用壳 | 3–8(不改 ticket 协议) | +| 管理闭环 B(含复制/作废/版本) | 4, 5, 6, 7 | +| 双入口 + 单条审批 | 4, 5 | +| 不做分享/转合同/PDF/导出/批量/审批人设置 | 4–7 禁止;Task 9 回归 | +| 全字段表单 + page variant | 6 | +| 提交时选手 + 预填默认人 | 5 Step 3 | +| Tab + keyword | 1, 4 | +| 三墙样式 | 4–7 `mp-webview.scss` | +| 菜单 + 角色补绑 | 2 | +| 工作台 + route map | 8 | +| 双权限 | 2 + 8 + 现有 API | +| 接入文档 | 9 | + +**占位符扫描:** 无 TBD;审批人列表数据源、`SubmitApprovalForm` 字段要求实现时打开现有桌面代码核对(已写明路径)。 + +--- + +## 执行说明 + +实施前建议在隔离 worktree(`using-git-worktrees`)。每 Task 完成后跑对应单测/手工点,再 commit。后端无新接口;本地需能访问现有 CRM 报价 API。测试库执行菜单 SQL。