2026-07-02 6e312c64c056acf479a6dd8393dd42a80b69b7d4
docs/crm_erp_integration_frontend.md
@@ -1,46 +1,52 @@
# CRM 与 ERP 销售一体化改造 - 前端文档
# CRM 与 ERP 销售一体化改造 - 前端文档(完整版)
## 概述
本次改造将 CRM 客户作为统一客户主数据,ERP 销售模块(销售订单、销售出库、销售退货、收款单)统一使用 CRM 客户。
本次改造实现 CRM 与 ERP 销售模块的深度集成,包括:
- **Phase 1**:客户数据统一 - ERP 使用 CRM 客户
- **Phase 2**:产品数据统一 - CRM/ERP 使用 MDM 物料
- **Phase 3**:合同生成销售订单
- **Phase 4**:销售全流程追溯
- **Phase 5**:营销数据报表
- **Phase 6**:数据迁移与清理(SQL 部分)
---
## 一、改造清单
## 一、改造清单汇总
### 后端改造(已完成)
### 1.1 新增模块
| 模块 | 说明 |
|------|------|
| `yudao-module-crm-api` | CRM API 模块(客户 API、报表 API、追溯 API) |
| `yudao-module-erp-api` | ERP API 模块(销售订单 API) |
| `yudao-module-mdm-api` | MDM API 模块(物料 API) |
### 1.2 后端改造文件
| 文件 | 改动说明 |
|------|----------|
| `yudao-module-crm-api/` | 新增 CRM 客户 API 模块 |
| `CrmCustomerApi.java` | 新增客户 API 接口 |
| `CrmCustomerRespDTO.java` | 新增客户 DTO |
| `CrmCustomerApiImpl.java` | 实现 CRM 客户 API |
| `CrmCustomerApi.java` | CRM 客户 API 接口 |
| `CrmCustomerRespDTO.java` | 客户响应 DTO |
| `CrmReportApi.java` | 营销报表 API 接口 |
| `CrmSaleTraceApi.java` | 销售追溯 API 接口 |
| `ErpSaleOrderApi.java` | 销售订单 API 接口 |
| `MdmItemApi.java` | MDM 物料 API 接口 |
| `ErpSaleOrderServiceImpl.java` | 使用 CRM 客户 API |
| `ErpFinanceReceiptServiceImpl.java` | 使用 CRM 客户 API |
| `ErpSaleOrderController.java` | 使用 CRM 客户 API |
| `ErpSaleOrderDO.java` | 更新注释关联 CRM 客户 |
| `ErpSaleOutDO.java` | 更新注释关联 CRM 客户 |
| `ErpSaleReturnDO.java` | 更新注释关联 CRM 客户 |
| `ErpFinanceReceiptDO.java` | 更新注释关联 CRM 客户 |
### 后端改造(待完成)
| 文件 | 改动说明 |
|------|----------|
| `ErpSaleOutController.java` | 使用 CRM 客户 API |
| `ErpSaleReturnController.java` | 使用 CRM 客户 API |
| `ErpFinanceReceiptController.java` | 使用 CRM 客户 API |
| `CrmContractServiceImpl.java` | 新增生成销售订单方法 |
| `CrmProductServiceImpl.java` | 使用 MDM 物料 API |
| `ErpProductServiceImpl.java` | 使用 MDM 物料 API |
---
## 二、涉及页面
### ERP 销售模块
### 2.1 ERP 销售模块
| 页面 | 路径 | 改动说明 |
|------|------|----------|
| 销售订单列表 | `/erp/sale/order/index` | 客户选择改为 CRM 客户选择组件 |
| 销售订单列表 | `/erp/sale/order/index` | 客户选择改为 CRM 客户,新增关联合同列 |
| 销售订单表单 | `/erp/sale/order/form` | 客户选择改为 CRM 客户选择组件 |
| 销售出库列表 | `/erp/sale/out/index` | 客户选择改为 CRM 客户选择组件 |
| 销售出库表单 | `/erp/sale/out/form` | 客户选择改为 CRM 客户选择组件 |
@@ -49,35 +55,43 @@
| 收款单列表 | `/erp/finance/receipt/index` | 客户选择改为 CRM 客户选择组件 |
| 收款单表单 | `/erp/finance/receipt/form` | 客户选择改为 CRM 客户选择组件 |
### 2.2 CRM 合同模块
| 页面 | 路径 | 改动说明 |
|------|------|----------|
| 合同列表 | `/crm/contract/index` | 添加"生成销售订单"按钮 |
| 合同详情 | `/crm/contract/detail` | 显示销售订单信息、销售流程追溯 |
### 2.3 CRM 商机模块
| 页面 | 路径 | 改动说明 |
|------|------|----------|
| 商机详情 | `/crm/business/detail` | 添加销售流程追溯入口 |
### 2.4 CRM 客户模块
| 页面 | 路径 | 改动说明 |
|------|------|----------|
| 客户详情 | `/crm/customer/detail` | 添加销售流程追溯列表 |
### 2.5 CRM 报表模块(新增)
| 页面 | 路径 | 说明 |
|------|------|------|
| 报表概览 | `/crm/report/dashboard` | 数据概览页面 |
---
## 三、客户选择组件
## 三、API 接口
### 3.1 使用 CRM 客户选择组件
ERP 销售模块的客户选择统一使用 CRM 客户选择组件。
```html
<!-- 原来:ERP 客户选择 -->
<el-select v-model="form.customerId" placeholder="请选择客户">
  <el-option
    v-for="item in customerList"
    :key="item.id"
    :label="item.name"
    :value="item.id"
  />
</el-select>
<!-- 改为:CRM 客户选择组件 -->
<CrmCustomerSelect v-model="form.customerId" />
```
### 3.2 CRM 客户 API
### 3.1 CRM 客户 API
| 方法 | 路径 | 说明 |
|------|------|------|
| GET | /crm/customer/simple-list | 获取客户精简列表(用于下拉选择) |
| GET | /crm/customer/list-by-ids | 批量获取客户信息 |
| GET | /rpc-api/crm/customer/get | 获取单个客户 |
| POST | /rpc-api/crm/customer/validate | 校验客户存在 |
**客户精简列表响应:**
@@ -96,26 +110,670 @@
}
```
---
### 3.2 CRM 合同 API(新增)
## 四、废弃的 ERP 客户管理
| 方法 | 路径 | 说明 |
|------|------|------|
| POST | /crm/contract/generate-sale-order | 根据合同生成销售订单 |
### 4.1 ERP 客户管理页面废弃
**请求参数:**
以下页面标记为废弃,建议在菜单中隐藏或删除:
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| id | Long | 是 | 合同编号 |
- `/erp/customer/index` - ERP 客户列表
- `/erp/customer/form` - ERP 客户表单
**响应:**
```json
{
  "code": 0,
  "data": 12345,
  "msg": "操作成功"
}
```
### 3.3 CRM 销售追溯 API(新增)
| 方法 | 路径 | 说明 |
|------|------|------|
| GET | /rpc-api/crm/sale-trace/get-by-business-id | 根据商机编号获取追溯信息 |
| GET | /rpc-api/crm/sale-trace/get-by-contract-id | 根据合同编号获取追溯信息 |
| GET | /rpc-api/crm/sale-trace/get-list-by-customer-id | 根据客户编号获取追溯列表 |
**响应示例:**
```json
{
  "code": 0,
  "data": {
    "businessId": 1,
    "businessName": "商机A",
    "businessPrice": 100000,
    "customerId": 1,
    "customerName": "客户A",
    "contractId": 1,
    "contractName": "合同A",
    "contractPrice": 95000,
    "contractStatus": 20,
    "orderId": 1,
    "orderNo": "SO001",
    "orderPrice": 95000,
    "receivablePrice": 50000,
    "receivableItems": [
      { "receivableId": 1, "receivableNo": "RK001", "price": 30000 }
    ]
  }
}
```
### 3.4 CRM 营销报表 API(新增)
| 方法 | 路径 | 说明 |
|------|------|------|
| GET | /rpc-api/crm/report/get-customer-analyze | 获取客户分析统计 |
| GET | /rpc-api/crm/report/get-business-analyze | 获取商机分析统计 |
| GET | /rpc-api/crm/report/get-contract-analyze | 获取合同分析统计 |
| GET | /rpc-api/crm/report/get-sales-funnel | 获取销售漏斗统计 |
| GET | /rpc-api/crm/report/get-sales-performance | 获取销售业绩统计 |
**客户分析响应:**
```json
{
  "code": 0,
  "data": {
    "totalCount": 1000,
    "todayCount": 15,
    "dealCount": 300,
    "dealRate": 30.00,
    "lockCount": 50,
    "poolCount": 200
  }
}
```
**商机分析响应:**
```json
{
  "code": 0,
  "data": {
    "totalCount": 500,
    "todayCount": 8,
    "totalPrice": 5000000.00,
    "successCount": 150,
    "successPrice": 2000000.00,
    "successRate": 30.00,
    "processingCount": 250,
    "failCount": 100
  }
}
```
**合同分析响应:**
```json
{
  "code": 0,
  "data": {
    "totalCount": 400,
    "todayCount": 5,
    "totalPrice": 3000000.00,
    "approveCount": 300,
    "approvePrice": 2500000.00,
    "processCount": 20,
    "receivablePrice": 1500000.00,
    "receivableRate": 60.00
  }
}
```
**销售漏斗响应:**
```json
{
  "code": 0,
  "data": {
    "stages": [
      { "name": "客户", "count": 1000, "price": 0, "conversionRate": 100.00 },
      { "name": "商机", "count": 500, "price": 5000000.00, "conversionRate": 50.00 },
      { "name": "合同", "count": 300, "price": 2500000.00, "conversionRate": 30.00 },
      { "name": "回款", "count": null, "price": 1500000.00, "conversionRate": 15.00 }
    ]
  }
}
```
**销售业绩响应:**
```json
{
  "code": 0,
  "data": {
    "items": [
      {
        "userId": 1,
        "userName": "张三",
        "deptName": "销售一部",
        "contractCount": 50,
        "contractPrice": 500000.00,
        "receivablePrice": 300000.00,
        "businessCount": 80,
        "businessPrice": 800000.00
      }
    ]
  }
}
```
### 3.5 ERP 销售订单 API(新增 RPC)
| 方法 | 路径 | 说明 |
|------|------|------|
| POST | /rpc-api/erp/sale-order/create | 创建销售订单 |
| GET | /rpc-api/erp/sale-order/get | 获取销售订单 |
| GET | /rpc-api/erp/sale-order/get-by-contract-id | 根据合同编号获取订单 |
### 3.6 MDM 物料 API
| 方法 | 路径 | 说明 |
|------|------|------|
| POST | /rpc-api/mdm/item/create | 创建物料 |
| PUT | /rpc-api/mdm/item/update | 更新物料 |
| GET | /rpc-api/mdm/item/get | 获取物料 |
| POST | /rpc-api/mdm/item/list | 获取物料列表 |
| GET | /rpc-api/mdm/item/validate-exists | 校验物料存在 |
---
## 五、SQL 执行
## 四、前端修改点
见 `docs/crm_erp_integration_sql.sql`
### 4.1 客户选择组件
ERP 销售模块的客户选择统一使用 CRM 客户选择组件。
```html
<!-- 原来:ERP 客户选择 -->
<el-select v-model="form.customerId" placeholder="请选择客户">
  <el-option
    v-for="item in customerList"
    :key="item.id"
    :label="item.name"
    :value="item.id"
  />
</el-select>
<!-- 改为:CRM 客户选择组件 -->
<CrmCustomerSelect v-model="form.customerId" />
```
**组件实现:**
```html
<!-- CrmCustomerSelect.vue -->
<template>
  <el-select
    v-model="selectedValue"
    placeholder="请选择客户"
    filterable
    remote
    :remote-method="searchCustomer"
    @focus="loadCustomers"
  >
    <el-option
      v-for="item in customerList"
      :key="item.id"
      :label="item.name"
      :value="item.id"
    >
      <span>{{ item.name }}</span>
      <span style="color: #999; margin-left: 8px;">{{ item.mobile }}</span>
    </el-option>
  </el-select>
</template>
<script>
import { getCustomerSimpleList } from '@/api/crm/customer'
export default {
  props: {
    value: [Number, String]
  },
  data() {
    return {
      customerList: [],
      selectedValue: this.value
    }
  },
  watch: {
    value(val) {
      this.selectedValue = val
    },
    selectedValue(val) {
      this.$emit('input', val)
    }
  },
  methods: {
    async loadCustomers() {
      if (this.customerList.length === 0) {
        const { data } = await getCustomerSimpleList()
        this.customerList = data
      }
    },
    async searchCustomer(query) {
      if (query) {
        // 可选:实现远程搜索
      }
    }
  }
}
</script>
```
### 4.2 合同列表页面
**文件路径:** `/src/views/crm/contract/index.vue`
**新增按钮:**
```html
<!-- 在合同操作列添加 -->
<el-table-column label="操作" width="200">
  <template #default="{ row }">
    <el-button
      v-if="row.auditStatus === 20 && !row.orderId"
      type="success"
      size="small"
      @click="handleGenerateOrder(row)"
    >
      生成销售订单
    </el-button>
    <el-button
      v-if="row.orderId"
      type="primary"
      size="small"
      @click="handleViewOrder(row)"
    >
      查看订单
    </el-button>
  </template>
</el-table-column>
```
**新增方法:**
```js
import { generateSaleOrder } from '@/api/crm/contract'
methods: {
  async handleGenerateOrder(row) {
    try {
      await this.$confirm('确认根据该合同生成销售订单?', '提示')
      const { data } = await generateSaleOrder(row.id)
      this.$message.success('生成销售订单成功')
      this.getList()
    } catch (e) {
      if (e !== 'cancel') {
        this.$message.error(e.message || '生成失败')
      }
    }
  },
  handleViewOrder(row) {
    this.$router.push({
      path: '/erp/sale/order/detail',
      query: { id: row.orderId }
    })
  }
}
```
### 4.3 销售订单列表页面
**文件路径:** `/src/views/erp/sale/order/index.vue`
**新增列:**
```html
<el-table-column label="关联合同" prop="contractNo" width="150">
  <template #default="{ row }">
    <el-link
      v-if="row.contractId"
      type="primary"
      @click="handleViewContract(row)"
    >
      {{ row.contractNo }}
    </el-link>
    <span v-else>-</span>
  </template>
</el-table-column>
```
### 4.4 商机详情页面 - 销售追溯
**文件路径:** `/src/views/crm/business/detail.vue`
```html
<el-card header="销售流程追溯">
  <el-timeline v-if="traceInfo">
    <el-timeline-item
      :timestamp="traceInfo.businessCreateTime"
      color="#409EFF"
    >
      <el-card>
        <h4>商机创建</h4>
        <p>商机名称:{{ traceInfo.businessName }}</p>
        <p>商机金额:¥{{ traceInfo.businessPrice }}</p>
      </el-card>
    </el-timeline-item>
    <el-timeline-item
      v-if="traceInfo.contractId"
      :timestamp="traceInfo.contractCreateTime"
      color="#67C23A"
    >
      <el-card>
        <h4>合同签订</h4>
        <p>合同名称:{{ traceInfo.contractName }}</p>
        <p>合同金额:¥{{ traceInfo.contractPrice }}</p>
      </el-card>
    </el-timeline-item>
    <el-timeline-item
      v-if="traceInfo.orderId"
      :timestamp="traceInfo.orderCreateTime"
      color="#E6A23C"
    >
      <el-card>
        <h4>销售订单</h4>
        <p>订单号:
          <el-link type="primary" @click="viewOrder(traceInfo.orderId)">
            {{ traceInfo.orderNo }}
          </el-link>
        </p>
        <p>订单金额:¥{{ traceInfo.orderPrice }}</p>
      </el-card>
    </el-timeline-item>
    <el-timeline-item v-if="traceInfo.receivablePrice > 0" color="#F56C6C">
      <el-card>
        <h4>回款记录</h4>
        <p>已回款金额:¥{{ traceInfo.receivablePrice }}</p>
      </el-card>
    </el-timeline-item>
  </el-timeline>
  <el-empty v-else description="暂无销售流程数据" />
</el-card>
```
### 4.5 报表概览页面
**文件路径:** `/src/views/crm/report/dashboard.vue`
```html
<template>
  <div class="report-dashboard">
    <!-- 统计卡片 -->
    <el-row :gutter="20">
      <el-col :span="6">
        <el-card class="stat-card">
          <div class="stat-title">客户总数</div>
          <div class="stat-value">{{ customerAnalyze.totalCount }}</div>
          <div class="stat-footer">今日新增:{{ customerAnalyze.todayCount }}</div>
        </el-card>
      </el-col>
      <el-col :span="6">
        <el-card class="stat-card">
          <div class="stat-title">商机总数</div>
          <div class="stat-value">{{ businessAnalyze.totalCount }}</div>
          <div class="stat-footer">转化率:{{ businessAnalyze.successRate }}%</div>
        </el-card>
      </el-col>
      <el-col :span="6">
        <el-card class="stat-card">
          <div class="stat-title">合同金额</div>
          <div class="stat-value">¥{{ contractAnalyze.totalPrice }}</div>
          <div class="stat-footer">已回款:¥{{ contractAnalyze.receivablePrice }}</div>
        </el-card>
      </el-col>
      <el-col :span="6">
        <el-card class="stat-card">
          <div class="stat-title">回款率</div>
          <div class="stat-value">{{ contractAnalyze.receivableRate }}%</div>
        </el-card>
      </el-col>
    </el-row>
    <!-- 销售业绩排行 -->
    <el-card header="销售业绩排行">
      <el-table :data="performanceData.items" stripe>
        <el-table-column prop="userName" label="销售人员" />
        <el-table-column prop="deptName" label="部门" />
        <el-table-column prop="contractCount" label="合同数量" width="100" />
        <el-table-column prop="contractPrice" label="合同金额" width="150" />
        <el-table-column prop="receivablePrice" label="回款金额" width="150" />
      </el-table>
    </el-card>
  </div>
</template>
<script>
import {
  getCustomerAnalyze,
  getBusinessAnalyze,
  getContractAnalyze,
  getSalesPerformance
} from '@/api/crm/report'
export default {
  data() {
    return {
      customerAnalyze: {},
      businessAnalyze: {},
      contractAnalyze: {},
      performanceData: { items: [] }
    }
  },
  created() {
    this.loadData()
  },
  methods: {
    async loadData() {
      const [c, b, ct, p] = await Promise.all([
        getCustomerAnalyze(),
        getBusinessAnalyze(),
        getContractAnalyze(),
        getSalesPerformance()
      ])
      this.customerAnalyze = c.data
      this.businessAnalyze = b.data
      this.contractAnalyze = ct.data
      this.performanceData = p.data
    }
  }
}
</script>
```
### 4.6 API 定义文件
**文件路径:** `/src/api/crm/contract.js`
```js
// 生成销售订单
export function generateSaleOrder(id) {
  return request({
    url: '/crm/contract/generate-sale-order',
    method: 'post',
    params: { id }
  })
}
```
**文件路径:** `/src/api/crm/saleTrace.js`
```js
// 根据商机编号获取销售流程追溯信息
export function getTraceByBusinessId(businessId) {
  return request({
    url: '/crm/sale-trace/get-by-business-id',
    method: 'get',
    params: { businessId }
  })
}
// 根据合同编号获取销售流程追溯信息
export function getTraceByContractId(contractId) {
  return request({
    url: '/crm/sale-trace/get-by-contract-id',
    method: 'get',
    params: { contractId }
  })
}
// 根据客户编号获取销售流程追溯列表
export function getTraceListByCustomerId(customerId) {
  return request({
    url: '/crm/sale-trace/get-list-by-customer-id',
    method: 'get',
    params: { customerId }
  })
}
```
**文件路径:** `/src/api/crm/report.js`
```js
// 获取客户分析统计
export function getCustomerAnalyze(startTime, endTime) {
  return request({
    url: '/crm/report/get-customer-analyze',
    method: 'get',
    params: { startTime, endTime }
  })
}
// 获取商机分析统计
export function getBusinessAnalyze(startTime, endTime) {
  return request({
    url: '/crm/report/get-business-analyze',
    method: 'get',
    params: { startTime, endTime }
  })
}
// 获取合同分析统计
export function getContractAnalyze(startTime, endTime) {
  return request({
    url: '/crm/report/get-contract-analyze',
    method: 'get',
    params: { startTime, endTime }
  })
}
// 获取销售漏斗统计
export function getSalesFunnel() {
  return request({
    url: '/crm/report/get-sales-funnel',
    method: 'get'
  })
}
// 获取销售业绩统计
export function getSalesPerformance(startTime, endTime) {
  return request({
    url: '/crm/report/get-sales-performance',
    method: 'get',
    params: { startTime, endTime }
  })
}
```
---
## 六、改造完成状态
## 五、数据模型变更
### 5.1 CRM 合同表
```sql
ALTER TABLE `crm_contract`
ADD COLUMN `order_id` bigint DEFAULT NULL COMMENT '关联 ERP 销售订单编号' AFTER `total_price`,
ADD COLUMN `order_no` varchar(32) DEFAULT NULL COMMENT 'ERP 销售订单单号' AFTER `order_id`;
```
### 5.2 CRM 商机表
```sql
ALTER TABLE `crm_business`
ADD COLUMN `order_id` bigint DEFAULT NULL COMMENT '关联 ERP 销售订单编号' AFTER `total_price`,
ADD COLUMN `order_no` varchar(32) DEFAULT NULL COMMENT 'ERP 销售订单单号' AFTER `order_id`;
```
### 5.3 ERP 销售订单表
```sql
ALTER TABLE `erp_sale_order`
ADD COLUMN `contract_id` bigint DEFAULT NULL COMMENT '关联 CRM 合同编号' AFTER `customer_id`,
ADD COLUMN `contract_no` varchar(32) DEFAULT NULL COMMENT 'CRM 合同单号' AFTER `contract_id`;
```
---
## 六、业务规则
### 6.1 生成销售订单条件
- 合同必须审批通过(auditStatus = 20)
- 合同未关联销售订单(orderId 为空)
- 合同必须包含产品信息
### 6.2 客户数据规则
- ERP 销售模块统一使用 CRM 客户
- 原 ERP 客户表(erp_customer)废弃
- 客户选择通过 CRM 客户 API 获取
### 6.3 产品数据规则
- CRM 和 ERP 产品统一使用 MDM 物料(itemType=2)
- MDM 物料类型:1-物料,2-产品,3-半成品
---
## 七、业务流程图
```
客户管理(CRM) → 商机跟进(CRM) → 合同签订(CRM) → 合同审批
                                                      ↓
                                              生成销售订单(ERP)
                                                      ↓
                                              订单审批(ERP)
                                                      ↓
                                              销售出库(ERP)
                                                      ↓
                                              客户回款(ERP)
```
**销售漏斗转化:**
```
客户(1000) → 商机(500) → 合同(300) → 回款(150)
    ↓            ↓            ↓           ↓
  100%        50%          30%         15%
```
---
## 八、注意事项
1. **数据权限**:报表数据需要根据用户权限过滤
2. **状态控制**:只有审批通过的合同才能生成订单
3. **唯一性**:一个合同只能生成一个销售订单
4. **数据同步**:生成订单后,合同与订单双向关联
5. **废弃页面**:ERP 客户管理页面(/erp/customer/index)建议在菜单中隐藏
---
## 九、完成状态
| Phase | 内容 | 状态 |
|-------|------|------|
@@ -128,13 +786,223 @@
---
## 七、产出文档清单
## 十一、销售报价模块(新增)
### 11.1 涉及页面
| 页面 | 路径 | 说明 |
|------|------|------|
| 报价单列表 | `/crm/sale-quotation/index` | 报价单列表、操作按钮 |
| 报价单表单 | `/crm/sale-quotation/form` | 新建/编辑报价单 |
| 报价单详情 | `/crm/sale-quotation/detail` | 查看详情、审批进度 |
### 11.2 API 接口
| 方法 | 路径 | 说明 |
|------|------|------|
| POST | /crm/sale-quotation/create | 创建报价单 |
| PUT | /crm/sale-quotation/update | 更新报价单 |
| DELETE | /crm/sale-quotation/delete | 删除报价单 |
| GET | /crm/sale-quotation/get | 获取报价单详情 |
| GET | /crm/sale-quotation/page | 分页查询 |
| GET | /crm/sale-quotation/page-by-customer | 按客户分页 |
| GET | /crm/sale-quotation/page-by-business | 按商机分页 |
| PUT | /crm/sale-quotation/submit | 提交审批(需传 processDefinitionKey) |
| GET | /crm/sale-quotation/approve-process-list | 获取可选审批流程列表 |
| POST | /crm/sale-quotation/convert-contract | 转为合同 |
**请求参数示例**(创建报价单):
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| name | String | 是 | 报价单名称 |
| customerId | Long | 是 | 客户编号 |
| businessId | Long | 否 | 商机编号 |
| contactId | Long | 否 | 联系人编号 |
| quotationTime | LocalDateTime | 否 | 报价日期 |
| validUntil | LocalDateTime | 否 | 有效期至 |
| discountPercent | BigDecimal | 否 | 整单折扣 |
| taxRate | BigDecimal | 否 | 税率 |
| fileUrl | String | 否 | 附件地址 |
| remark | String | 否 | 备注 |
| items | List | 是 | 物料明细列表 |
**物料明细 Item**:
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| itemId | Long | 是 | MDM 物料编号 |
| quotationPrice | BigDecimal | 是 | 报价单价 |
| count | BigDecimal | 是 | 数量 |
| taxPercent | BigDecimal | 否 | 税率 |
| remark | String | 否 | 备注 |
### 11.3 前端修改点
#### 报价单列表页面
**文件路径:** `/src/views/crm/sale-quotation/index.vue`
```html
<!-- 操作列 -->
<el-table-column label="操作" width="280">
  <template #default="{ row }">
    <el-button v-if="row.status === 0" type="primary" size="small" @click="handleUpdate(row)">编辑</el-button>
    <el-button v-if="row.status === 0" type="success" size="small" @click="handleSubmit(row)">提交审批</el-button>
    <el-button v-if="row.status === 20 && !row.contractId" type="warning" size="small" @click="handleConvertContract(row)">转合同</el-button>
    <el-button v-if="row.contractId" type="primary" size="small" @click="handleViewContract(row)">查看合同</el-button>
    <el-button v-if="row.status === 0" type="danger" size="small" @click="handleDelete(row)">删除</el-button>
  </template>
</el-table-column>
```
**提交审批弹窗**:
```html
<el-dialog title="选择审批流程" v-model="submitDialogVisible" width="400px">
  <el-form label-width="100px">
    <el-form-item label="审批流程" required>
      <el-select v-model="selectedProcessKey" placeholder="请选择" style="width: 100%">
        <el-option v-for="item in processList" :key="item.key" :label="item.name" :value="item.key" />
      </el-select>
    </el-form-item>
  </el-form>
  <template #footer>
    <el-button @click="submitDialogVisible = false">取消</el-button>
    <el-button type="primary" @click="confirmSubmit" :loading="submitLoading">确定</el-button>
  </template>
</el-dialog>
```
**JS 方法**:
```js
import { getSaleQuotationApproveProcessList, submitSaleQuotation, convertSaleQuotationToContract } from '@/api/crm/saleQuotation'
methods: {
  async handleSubmit(row) {
    const { data } = await getSaleQuotationApproveProcessList()
    this.processList = data
    this.currentRow = row
    this.submitDialogVisible = true
  },
  async confirmSubmit() {
    if (!this.selectedProcessKey) {
      this.$message.warning('请选择审批流程')
      return
    }
    this.submitLoading = true
    try {
      await submitSaleQuotation(this.currentRow.id, this.selectedProcessKey)
      this.$message.success('提交成功')
      this.submitDialogVisible = false
      this.getList()
    } finally {
      this.submitLoading = false
    }
  },
  async handleConvertContract(row) {
    try {
      await this.$confirm('确认将该报价单转为合同?', '提示')
      const { data } = await convertSaleQuotationToContract(row.id)
      this.$message.success('转合同成功')
      this.$router.push({ path: '/crm/contract/detail', query: { id: data } })
    } catch (e) {
      if (e !== 'cancel') this.$message.error(e.message || '转合同失败')
    }
  }
}
```
#### 报价单表单页面 - MDM 物料选择
**文件路径:** `/src/views/crm/sale-quotation/form.vue`
由于报价单使用 MDM 物料,需要使用 MDM 物料选择组件:
```html
<MdmItemSelect v-model="item.itemId" @change="handleItemChange" />
```
**API 定义文件**:`/src/api/crm/saleQuotation.js`
```js
import request from '@/utils/request'
// 创建报价单
export function createSaleQuotation(data) {
  return request({ url: '/crm/sale-quotation/create', method: 'post', data })
}
// 更新报价单
export function updateSaleQuotation(data) {
  return request({ url: '/crm/sale-quotation/update', method: 'put', data })
}
// 删除报价单
export function deleteSaleQuotation(ids) {
  return request({ url: '/crm/sale-quotation/delete', method: 'delete', params: { ids } })
}
// 获取报价单详情
export function getSaleQuotation(id) {
  return request({ url: '/crm/sale-quotation/get', method: 'get', params: { id } })
}
// 分页查询
export function getSaleQuotationPage(params) {
  return request({ url: '/crm/sale-quotation/page', method: 'get', params })
}
// 提交审批
export function submitSaleQuotation(id, processDefinitionKey) {
  return request({ url: '/crm/sale-quotation/submit', method: 'put', params: { id, processDefinitionKey } })
}
// 获取审批流程列表
export function getSaleQuotationApproveProcessList() {
  return request({ url: '/crm/sale-quotation/approve-process-list', method: 'get' })
}
// 转合同
export function convertSaleQuotationToContract(id) {
  return request({ url: '/crm/sale-quotation/convert-contract', method: 'post', params: { id } })
}
```
### 11.4 业务流程
```
商机(可选) → 销售报价(草稿) → 提交审批 → 审批中 → 审批通过 → 转合同
                                    ↓
                              审批拒绝 → 重新提交或修改
```
### 11.5 注意事项
1. 报价单和合同统一使用 **MDM 物料**(`MdmItemApi`),与 ERP 销售订单一致
2. BPM 分类编码 `sale_quotation_approve` 需要在 BPM 管理后台创建
3. 报价单转合同时,物料明细直接复制到合同产品表(字段一致)
4. 报价单号前缀为 `QT`,格式:QT + yyyyMMdd + 6位自增序号
5. 商机关联为可选,报价单可以独立创建不依赖商机
### 11.6 合同产品字段变更说明
合同产品表 `crm_contract_product` 已改为使用 MDM 物料:
| 原字段 | 新字段 | 说明 |
|--------|--------|------|
| `product_id` | `item_id` | MDM 物料编号 |
| `product_price` | `item_price` | 物料原价 |
| - | `item_unit_id` | 计量单位编号 |
前端表单需要使用 MDM 物料选择组件替代 CRM 产品选择组件。
---
## 十、产出文档清单
| 文档 | 说明 |
|------|------|
| `docs/crm_erp_integration_frontend.md` | CRM 客户一体化前端文档 |
| `docs/crm_erp_integration_frontend.md` | 本文档 |
| `docs/crm_erp_integration_sql.sql` | SQL 变更脚本 |
| `docs/mdm_product_integration_frontend.md` | MDM 产品一体化前端文档 |
| `docs/contract_generate_order_frontend.md` | 合同生成订单前端文档 |
| `docs/sales_traceability_frontend.md` | 销售追溯前端文档 |
| `docs/marketing_report_frontend.md` | 营销报表前端文档 |