|
|
@@ -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<MpQuoteView, 'all'>; 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<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 === '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
|
|
|
+<article
|
|
|
+ v-for="card in cards"
|
|
|
+ :key="card.id"
|
|
|
+ class="mp-wv-card"
|
|
|
+ @click="goDetail(card.id)"
|
|
|
+>
|
|
|
+ <div class="mp-wv-card__title">{{ card.quoteTitle }}</div>
|
|
|
+ <div class="mp-wv-card__meta">{{ card.quoteNo }} · {{ card.customerName }}</div>
|
|
|
+ <div class="mp-wv-card__meta">{{ card.amountText }} · {{ card.quoteDate }}</div>
|
|
|
+ <div class="mp-wv-card__tags">
|
|
|
+ <el-tag :type="card.quoteTypeTagType" size="small">{{ card.quoteTypeLabel }}</el-tag>
|
|
|
+ <el-tag :type="statusTagType(card.status)" size="small">{{ statusLabel(card.status) }}</el-tag>
|
|
|
+ </div>
|
|
|
+</article>
|
|
|
+```
|
|
|
+
|
|
|
+分页:与产品页一致「加载更多」(`pageNum++`)。
|
|
|
+
|
|
|
+403 → `forbidden` 显示「无权限」。
|
|
|
+
|
|
|
+- [ ] **Step 2: 薄壳页**
|
|
|
+
|
|
|
+`index.vue`:
|
|
|
+
|
|
|
+```vue
|
|
|
+<template>
|
|
|
+ <MpQuoteList mode="manage" />
|
|
|
+</template>
|
|
|
+<script setup lang="ts">
|
|
|
+ import MpQuoteList from './MpQuoteList.vue'
|
|
|
+</script>
|
|
|
+```
|
|
|
+
|
|
|
+`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
|
|
|
+<template>
|
|
|
+ <MpWebviewShell>
|
|
|
+ <header class="mp-wv-header">
|
|
|
+ <div class="mp-wv-header__row">
|
|
|
+ <button class="mp-wv-header__back" type="button" @click="goBack">←</button>
|
|
|
+ <h1 class="mp-wv-header__title">{{ pageTitle }}</h1>
|
|
|
+ </div>
|
|
|
+ </header>
|
|
|
+ <QuoteFormDialog
|
|
|
+ variant="page"
|
|
|
+ :model-value="true"
|
|
|
+ :page-mode="pageMode"
|
|
|
+ :quote-id="quoteId"
|
|
|
+ :copy-from-id="copyFromId"
|
|
|
+ @success="onSuccess"
|
|
|
+ @close="goBack"
|
|
|
+ />
|
|
|
+ </MpWebviewShell>
|
|
|
+</template>
|
|
|
+```
|
|
|
+
|
|
|
+- `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。
|