Files
shanxia2.0/docs/superpowers/plans/2026-10-15-mp-webview-crm-contract.md
T

20 KiB
Raw Blame History

小程序 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/contract*,提供管理闭环(列表/详情/增改删/提交审批/复制/作废·恢复/上传扫描件)与审批闭环(待审/已审 + 单条与批量通过驳回),不含调价函、打印/PDF、导出、执行、续签。

Architecture: 对齐报价 WebView(ticket → exchange → MpWebviewShell)。新建 views/m/crm/contract/*,样式 mp-webview.scss。业务 API 复用 @/api/crm/contract。桌面合同表单增加 variant="page"。列表支持多选批量。菜单 SQL 幂等写入 MINI_CRM_CONTRACT*,按 Web crm:contract:list / crm:contract:approve 分别补绑。

Tech Stack: Vue 3 · Vue Router hash · Element Plus · Pinia · uni-app web-view · MySQL · Node test(pnpm exec tsx --test)

Spec: docs/superpowers/specs/2026-10-15-mp-webview-crm-contract-design.md


文件结构

路径 职责
web-admin/src/utils/mp-crm-contract-card.ts Tab/查询/卡片纯函数
web-admin/src/utils/mp-crm-contract-card.test.ts 单测
web-admin/src/utils/mp-webview.ts MP_CRM_CONTRACT_LIST_PATH、MP_CRM_CONTRACT_APPROVAL_PATH
web-admin/src/views/m/crm/contract/MpContractList.vue 共享列表(含多选批量)
web-admin/src/views/m/crm/contract/index.vue 管理壳
web-admin/src/views/m/crm/contract/approval.vue 审批壳
web-admin/src/views/m/crm/contract/detail.vue 详情 + 动作
web-admin/src/views/m/crm/contract/form.vue 表单路由壳
web-admin/src/views/crm/contract/components/ContractFormDialog.vue variant=page
web-admin/src/router/routes/staticRoutes.ts /m/crm/contract*
miniprogram/config/workbench-menus.js 两项入口
miniprogram/config/api.js WORKBENCH_ROUTE_MAP
docs/sql/migrations/2026-10-15-mini-crm-contract-webview-menus.sql 菜单 + 补绑
docs/web-admin-mp-webview-modules.md 合同对照行

明确不改: mp-webview 鉴权协议、后端合同业务语义、桌面合同默认 dialog 行为、调价函。

查询语义:

mode view 请求
manage all excludeStatus=invalid
manage approving status=approving
manage approved status=approved
approve submitted pendingApprovalUserId=currentUserId
approve approved approvedByUserId=currentUserId

分页:pageNum / pageSize。


Task 1: 列表卡片纯函数 + 单测

Files:

  • Create: web-admin/src/utils/mp-crm-contract-card.ts

  • Create: web-admin/src/utils/mp-crm-contract-card.test.ts

  • Step 1: 写失败单测

// web-admin/src/utils/mp-crm-contract-card.test.ts
import assert from 'node:assert/strict'
import { describe, it } from 'node:test'
import {
  MP_CONTRACT_MANAGE_TABS,
  MP_CONTRACT_APPROVE_TABS,
  buildContractListQuery,
  buildContractCard,
  formatContractMoney
} from './mp-crm-contract-card'

describe('tabs', () => {
  it('manage all/approving/approved; approve submitted/approved', () => {
    assert.deepEqual(
      MP_CONTRACT_MANAGE_TABS.map((x) => x.value),
      ['all', 'approving', 'approved']
    )
    assert.deepEqual(
      MP_CONTRACT_APPROVE_TABS.map((x) => x.value),
      ['submitted', 'approved']
    )
  })
})

describe('buildContractListQuery', () => {
  it('manage all excludes invalid and trims keyword', () => {
    assert.deepEqual(
      buildContractListQuery({
        mode: 'manage',
        view: 'all',
        pageNum: 1,
        pageSize: 20,
        keyword: ' 合同 ',
        currentUserId: 'u1'
      }),
      { pageNum: 1, pageSize: 20, keyword: '合同', excludeStatus: 'invalid' }
    )
  })

  it('manage approving sets status only', () => {
    const q = buildContractListQuery({
      mode: 'manage',
      view: 'approving',
      pageNum: 1,
      pageSize: 20,
      keyword: '',
      currentUserId: 'u1'
    })
    assert.equal(q.status, 'approving')
    assert.equal('pendingApprovalUserId' in q, false)
  })

  it('manage approved sets status approved', () => {
    const q = buildContractListQuery({
      mode: 'manage',
      view: 'approved',
      pageNum: 1,
      pageSize: 20,
      keyword: '',
      currentUserId: 'u1'
    })
    assert.equal(q.status, 'approved')
    assert.equal('approvedByUserId' in q, false)
  })

  it('approve submitted uses pendingApprovalUserId', () => {
    const q = buildContractListQuery({
      mode: 'approve',
      view: 'submitted',
      pageNum: 1,
      pageSize: 20,
      keyword: '',
      currentUserId: 'u9'
    })
    assert.equal(q.pendingApprovalUserId, 'u9')
  })

  it('approve approved uses approvedByUserId', () => {
    const q = buildContractListQuery({
      mode: 'approve',
      view: 'approved',
      pageNum: 1,
      pageSize: 20,
      keyword: 'x',
      currentUserId: 'u9'
    })
    assert.equal(q.approvedByUserId, 'u9')
    assert.equal(q.keyword, 'x')
  })
})

describe('buildContractCard', () => {
  it('maps fields', () => {
    const card = buildContractCard({
      id: 'c1',
      contractNo: 'HT001',
      contractTitle: '硫酸销售合同',
      customerName: '甲公司',
      amount: 1000,
      freight: 50,
      barrelDeposit: 20,
      status: 'draft',
      signDate: '2026-10-01'
    })
    assert.equal(card.id, 'c1')
    assert.equal(card.contractNo, 'HT001')
    assert.equal(card.amountText, '¥1,070.00')
    assert.equal(card.status, 'draft')
  })
})

describe('formatContractMoney', () => {
  it('formats and handles empty', () => {
    assert.equal(formatContractMoney(1000), '¥1,000.00')
    assert.equal(formatContractMoney(undefined), '—')
  })
})
  • Step 2: 跑测确认失败
cd web-admin && pnpm exec tsx --test src/utils/mp-crm-contract-card.test.ts

Expected: FAIL / module not found

  • Step 3: 实现纯函数
// web-admin/src/utils/mp-crm-contract-card.ts
export type MpContractMode = 'manage' | 'approve'
export type MpContractManageView = 'all' | 'approving' | 'approved'
export type MpContractApproveView = 'submitted' | 'approved'
export type MpContractView = MpContractManageView | MpContractApproveView

export const MP_CONTRACT_MANAGE_TABS: Array<{ value: MpContractManageView; label: string }> = [
  { value: 'all', label: '全部' },
  { value: 'approving', label: '待审批' },
  { value: 'approved', label: '已审批' }
]

export const MP_CONTRACT_APPROVE_TABS: Array<{ value: MpContractApproveView; label: string }> = [
  { value: 'submitted', label: '待审批' },
  { value: 'approved', label: '已审批' }
]

export function formatContractMoney(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 buildContractListQuery(input: {
  mode: MpContractMode
  view: MpContractView
  pageNum: number
  pageSize: number
  keyword: string
  currentUserId: string
}): Record<string, unknown> {
  const params: Record<string, unknown> = {
    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 === 'approving') params.status = 'approving'
    else params.status = 'approved'
    return params
  }

  if (input.view === 'submitted') {
    params.pendingApprovalUserId = input.currentUserId
  } else {
    params.approvedByUserId = input.currentUserId
  }
  return params
}

export function buildContractCard(row: {
  id: string | number
  contractNo?: string
  contractTitle?: string
  customerName?: string
  amount?: number
  freight?: number
  barrelDeposit?: number
  status?: string
  signDate?: string
}) {
  const total =
    (Number(row.amount) || 0) + (Number(row.freight) || 0) + (Number(row.barrelDeposit) || 0)
  return {
    id: String(row.id),
    contractNo: row.contractNo || '—',
    contractTitle: row.contractTitle || '—',
    customerName: row.customerName || '—',
    amountText: formatContractMoney(total),
    status: row.status || '',
    signDate: row.signDate || '—'
  }
}

状态 Tag 在页面用 dictStore crm_contract_status。

  • Step 4: 跑测确认通过
cd web-admin && pnpm exec tsx --test src/utils/mp-crm-contract-card.test.ts
  • Step 5: Commit
git add web-admin/src/utils/mp-crm-contract-card.ts web-admin/src/utils/mp-crm-contract-card.test.ts
git commit -m "$(cat <<'EOF'
feat(web-admin): add mp CRM contract card query helpers.

EOF
)"

Task 2: 菜单 SQL

Files:

  • Create: docs/sql/migrations/2026-10-15-mini-crm-contract-webview-menus.sql

  • Step 1: 写入幂等 SQL(结构对齐报价脚本)

-- 小程序:CRM管理 → 合同管理 / 合同审批(WebView)
-- list 随 CRM3000000000000000000000000020 (crm:contract:list)
-- approve 随 CRM3000000000000000000000000021 (crm:contract: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_CONTRACT', '合同管理', 'MINI_CRM_ROOT', 'C', 'mp-webview', NULL,
  'mini:crm:contract:list',
  1, '1', '1', 'shanxia2.0', 4, '小程序 WebView CRM 合同管理',
  'system', '', NOW(), NULL, NOW(), 0
FROM DUAL
WHERE NOT EXISTS (
  SELECT 1 FROM `sys_menu` WHERE `id` = 'MINI_CRM_CONTRACT' 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_CONTRACT_APPROVAL', '合同审批', 'MINI_CRM_ROOT', 'C', 'mp-webview', NULL,
  'mini:crm:contract:approve',
  1, '1', '1', 'shanxia2.0', 5, '小程序 WebView CRM 合同审批',
  'system', '', NOW(), NULL, NOW(), 0
FROM DUAL
WHERE NOT EXISTS (
  SELECT 1 FROM `sys_menu` WHERE `id` = 'MINI_CRM_CONTRACT_APPROVAL' AND `del_flag` = 0
);

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-contract-webview|v1')),
  r.id,
  m.menu_id,
  IFNULL(NULLIF(r.tenant_no, ''), 'shanxia2.0'),
  0,
  '小程序CRM合同管理:随 Web crm:contract: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 = 'CRM3000000000000000000000000020'
 AND web.del_flag = 0
JOIN (
  SELECT '1' AS menu_id UNION ALL
  SELECT 'MINI_CRM_ROOT' UNION ALL
  SELECT 'MINI_CRM_CONTRACT'
) 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
  );

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-contract-approval-webview|v1')),
  r.id,
  m.menu_id,
  IFNULL(NULLIF(r.tenant_no, ''), 'shanxia2.0'),
  0,
  '小程序CRM合同审批:随 Web crm:contract: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 = 'CRM3000000000000000000000000021'
 AND web.del_flag = 0
JOIN (
  SELECT '1' AS menu_id UNION ALL
  SELECT 'MINI_CRM_ROOT' UNION ALL
  SELECT 'MINI_CRM_CONTRACT_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 不连库执行)
git add docs/sql/migrations/2026-10-15-mini-crm-contract-webview-menus.sql
git commit -m "$(cat <<'EOF'
docs(sql): add miniprogram CRM contract 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

  • Create shells under web-admin/src/views/m/crm/contract/

  • Step 1: 常量

export const MP_CRM_CONTRACT_LIST_PATH = '/m/crm/contract'
export const MP_CRM_CONTRACT_APPROVAL_PATH = '/m/crm/contract/approval'
  • Step 2: 路由(catch-all 前;顺序固定)
  {
    path: '/m/crm/contract',
    name: 'MpCrmContractList',
    component: () => import('@views/m/crm/contract/index.vue'),
    meta: { title: '合同管理', isHideTab: true, isMpWebview: true }
  },
  {
    path: '/m/crm/contract/approval',
    name: 'MpCrmContractApproval',
    component: () => import('@views/m/crm/contract/approval.vue'),
    meta: { title: '合同审批', isHideTab: true, isMpWebview: true }
  },
  {
    path: '/m/crm/contract/new',
    name: 'MpCrmContractNew',
    component: () => import('@views/m/crm/contract/form.vue'),
    meta: { title: '新增合同', isHideTab: true, isMpWebview: true }
  },
  {
    path: '/m/crm/contract/:id/edit',
    name: 'MpCrmContractEdit',
    component: () => import('@views/m/crm/contract/form.vue'),
    meta: { title: '编辑合同', isHideTab: true, isMpWebview: true }
  },
  {
    path: '/m/crm/contract/:id',
    name: 'MpCrmContractDetail',
    component: () => import('@views/m/crm/contract/detail.vue'),
    meta: { title: '合同详情', isHideTab: true, isMpWebview: true }
  },
  • Step 3: 最小壳 vue(MpWebviewShell + title;同报价 Task 3)

  • Step 4: Commit

git commit -m "$(cat <<'EOF'
feat(web-admin): register /m/crm/contract static routes.

EOF
)"

Task 4: 共享列表 + 多选批量

Files:

  • Create: web-admin/src/views/m/crm/contract/MpContractList.vue
  • Replace: index.vue / approval.vue 薄壳

对照:views/m/crm/quote/MpQuoteList.vue,并增加多选。

  • Step 1: 实现 MpContractList.vue
  1. Prop mode: 'manage' | 'approve'
  2. @ready 后 listContract(buildContractListQuery(...));解析 list|rows + total
  3. resolveCurrentUserId 从 MpQuoteList / ContractListPage 复制
  4. Tab + keyword + 卡片(buildContractCard + dict 状态 Tag)
  5. 多选:卡片左侧 checkbox;selectedIds;底栏「已选 N」
  6. manage:Speed Dial 新增;批量「提交审批」(打开 Sheet,仅草稿 id);可选批量作废(逐条 invalidContract,跳过不可作废)
  7. approve:批量通过/驳回(Sheet;驳回意见必填);对每条解析 pending approval id(对齐桌面:listContractApproval 找当前用户 pending),调用 approvePass / approveReject(打开 api/crm/contract/approval.ts 核对字段)
  8. 详情:/m/crm/contract/${id}?from=${mode}
  9. goToMiniWorkbench();mp-webview.scss;403→无权限
  10. 禁止:导出、打印、执行、续签、facet

批量提交 Sheet:getContractApproverSetting 预填 + fetchUserOptions;batchSubmitApproval({ ids, approverIds, remark })。

  • Step 2: 薄壳
<template><MpContractList mode="manage" /></template>

approval:mode="approve"。

  • Step 3: Commit
git commit -m "$(cat <<'EOF'
feat(web-admin): add mp webview CRM contract list pages.

EOF
)"

Task 5: 详情页动作

Files:

  • Create/Modify: web-admin/src/views/m/crm/contract/detail.vue

对照:views/m/crm/quote/detail.vue + 桌面 ContractDetailDialog 动作集合(排除执行/续签/打印)。

  • Step 1: 加载与展示

getContract + 明细 + listContractApproval;分区展示;扫描件 URL 可预览。

  • Step 2: 管理底栏
条件 按钮
draft 提交审批、编辑、删除(canDeleteByCreator)、作废、复制、上传扫描件
可作废状态(draft/approving/approved/executing 等,对齐桌面) 作废、复制、上传;可编辑则编辑
invalid 作废恢复、复制
永不出现 执行、续签、打印、审批通过驳回

动作 API:submitApproval、invalidContract、restoreContract、delContract、uploadFile/uploadAttachments(对照 UploadDialog.vue)、复制 → /new?copyFrom=。

  • Step 3: 审批底栏

待我审 → 通过/驳回 Sheet → approvePass/approveReject。

  • Step 4: Commit
git commit -m "$(cat <<'EOF'
feat(web-admin): add mp webview CRM contract detail actions.

EOF
)"

Task 6: 表单 variant="page"

Files:

  • Modify: web-admin/src/views/crm/contract/components/ContractFormDialog.vue(及内部 Panel 若有)
  • Create/Modify: web-admin/src/views/m/crm/contract/form.vue

对照报价 QuoteFormDialog page 模式与 views/m/crm/quote/form.vue。

  • Step 1: 默认 variant=dialog;page 模式无 el-dialog、单列布局、sticky 底栏(取消/保存)
  • Step 2: form.vue 支持 add / edit / copyFrom
  • Step 3: 验证全字段保存
  • Step 4: Commit
git commit -m "$(cat <<'EOF'
feat(web-admin): add page variant for contract form in mp webview.

EOF
)"

Task 7: 工作台入口

Files:

  • Modify: miniprogram/config/workbench-menus.js

  • Modify: miniprogram/config/api.js

  • Step 1: CRM menus 增加两项

{
  name: '合同管理',
  enabled: true,
  code: 'crm-contract-webview',
  icon: '📄',
  perms: 'mini:crm:contract:list'
},
{
  name: '合同审批',
  enabled: true,
  code: 'crm-contract-approval-webview',
  icon: '✅',
  perms: 'mini:crm:contract:approve'
}

desc 更新含「合同」。不要加调价函。

  • Step 2: WORKBENCH_ROUTE_MAP
'crm-contract-webview':
  PAGE_PATHS.mpWebview + '?path=' + encodeURIComponent('/m/crm/contract'),
'crm-contract-approval-webview':
  PAGE_PATHS.mpWebview + '?path=' + encodeURIComponent('/m/crm/contract/approval')
  • Step 3: Commit
git commit -m "$(cat <<'EOF'
feat(miniprogram): enable CRM contract webview workbench entries.

EOF
)"

Task 8: 文档 + 回归清单

Files:

  • Modify: docs/web-admin-mp-webview-modules.md

  • Step 1: 增加 CRM 合同对照

## CRM 合同对照

- 管理 `/m/crm/contract`,审批 `/m/crm/contract/approval`,详情 `/:id`,表单 `/new` `/:id/edit`
- 工作台 code `crm-contract-webview` / `crm-contract-approval-webview`
- perms `mini:crm:contract:list` / `mini:crm:contract:approve`
- 支持单条与批量提交/审批;含复制、作废/恢复、上传扫描件
- 不做:调价函、打印/PDF、导出、执行、续签

文首「已落地」补上合同。

  • Step 2: 回归(spec §10);测试库执行 Task 2 SQL

  • Step 3: Commit

git commit -m "$(cat <<'EOF'
docs: document CRM contract as mp webview module.

EOF
)"

Spec 覆盖自检

Spec 项 Task
嵌套 H5 3–7
管理闭环 B + 批量 4, 5
全字段表单 6
不做调价函/打印/导出/执行/续签 4–8 禁止
菜单 + 补绑 2
工作台 7
文档 8

执行说明

当前分支 V1.5 可直接实现(与报价同模式)。每 Task 完成后 commit。后端无新接口。测试库执行菜单 SQL。