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 <cursoragent@cursor.com>
This commit is contained in:
1 parent
88cb1a9f62
commit
121e431f68
1 file changed
+942
@@ -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。
|
||||
Reference in new issue
Block a user