|
|
@@ -0,0 +1,681 @@
|
|
|
+# 小程序 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: 写失败单测**
|
|
|
+
|
|
|
+```ts
|
|
|
+// 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: 跑测确认失败**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd web-admin && pnpm exec tsx --test src/utils/mp-crm-contract-card.test.ts
|
|
|
+```
|
|
|
+
|
|
|
+Expected: FAIL / module not found
|
|
|
+
|
|
|
+- [ ] **Step 3: 实现纯函数**
|
|
|
+
|
|
|
+```ts
|
|
|
+// 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: 跑测确认通过**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd web-admin && pnpm exec tsx --test src/utils/mp-crm-contract-card.test.ts
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 5: Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+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**(结构对齐报价脚本)
|
|
|
+
|
|
|
+```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 不连库执行)**
|
|
|
+
|
|
|
+```bash
|
|
|
+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: 常量**
|
|
|
+
|
|
|
+```ts
|
|
|
+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 前;顺序固定)**
|
|
|
+
|
|
|
+```ts
|
|
|
+ {
|
|
|
+ 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**
|
|
|
+
|
|
|
+```bash
|
|
|
+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: 薄壳**
|
|
|
+
|
|
|
+```vue
|
|
|
+<template><MpContractList mode="manage" /></template>
|
|
|
+```
|
|
|
+
|
|
|
+approval:`mode="approve"`。
|
|
|
+
|
|
|
+- [ ] **Step 3: Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+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**
|
|
|
+
|
|
|
+```bash
|
|
|
+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**
|
|
|
+
|
|
|
+```bash
|
|
|
+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 增加两项**
|
|
|
+
|
|
|
+```js
|
|
|
+{
|
|
|
+ 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**
|
|
|
+
|
|
|
+```js
|
|
|
+'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**
|
|
|
+
|
|
|
+```bash
|
|
|
+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 合同对照**
|
|
|
+
|
|
|
+```markdown
|
|
|
+## 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**
|
|
|
+
|
|
|
+```bash
|
|
|
+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。
|