# CRM 前端代码迁移替换指南 > **策略**:拷贝 CRM 原版 Vue 代码 → 机械替换框架差异 → 适配山峡2.0 设计体系 > **源系统**:山峡 CRM(RuoYi-Vue-Plus,`shanxia/crm/web/`) > **目标系统**:山峡一体化门户(`shanxia2.0/web-admin/`) > **版本**:v1.0 | 2026-05-30 --- ## 目录 - [1. 迁移模块清单与文件映射](#1-迁移模块清单与文件映射) - [2. 通用替换规则(所有文件必做)](#2-通用替换规则所有文件必做) - [3. 页面外层布局替换](#3-页面外层布局替换) - [4. 弹窗/抽屉替换规则](#4-弹窗抽屉替换规则) - [5. 表单布局替换规则](#5-表单布局替换规则) - [6. 图片/附件上传替换规则](#6-图片附件上传替换规则) - [7. 字典/状态展示替换规则](#7-字典状态展示替换规则) - [8. 分页组件替换规则](#8-分页组件替换规则) - [9. 工具栏与操作按钮替换规则](#9-工具栏与操作按钮替换规则) - [10. 消息提示/确认框替换规则](#10-消息提示确认框替换规则) - [11. 权限指令替换规则](#11-权限指令替换规则) - [12. API 对接规则](#12-api-对接规则) - [13. 各模块特殊注意事项](#13-各模块特殊注意事项) --- ## 1. 迁移模块清单与文件映射 ### 1.1 产品分类 | CRM 源文件 | 山峡2.0 目标位置 | 说明 | |---|---|---| | `crm/product/category/index.vue` (360行) | `views/crm/product-category/index.vue` | 树形 CRUD,弹窗内联 | > CRM 把新增/编辑弹窗内联在 index.vue 中,**保留内联方式**,删除山峡2.0 原有的 `CategoryFormDialog.vue`。 ### 1.2 产品管理 | CRM 源文件 | 山峡2.0 目标位置 | 说明 | |---|---|---| | `crm/product/index.vue` (849行) | `views/crm/product/index.vue` | 含表单弹窗+详情弹窗+导入导出,全部内联 | > 删除山峡2.0 原有的 `ProductFormDialog.vue` 和 `ProductViewDialog.vue`。 ### 1.3 客户管理 | CRM 源文件 | 山峡2.0 目标位置 | 说明 | |---|---|---| | `crm/customer/index.vue` (793行) | `views/crm/customer/index.vue` | 列表页,含视图切换+快速检索+列设置 | | `crm/customer/form.vue` (568行) | `views/crm/customer/CustomerFormDialog.vue` | 新增/编辑弹窗 | | `crm/customer/detail.vue` (1195行) | `views/crm/customer/CustomerDetailDialog.vue` | **详情抽屉→弹窗**,含联系人/跟进/操作日志 tab | | `crm/customer/transferForm.vue` | `views/crm/customer/TransferDialog.vue` | 转移弹窗 | | `crm/customer/intoPoolForm.vue` | `views/crm/customer/IntoPoolDialog.vue` | 放入公海弹窗 | | `crm/customer/followForm.vue` | `views/crm/customer/FollowFormDialog.vue` | 跟进记录弹窗 | | `crm/customer/dealForm.vue` | `views/crm/customer/DealDialog.vue` | 成交弹窗 | | `crm/customer/components/contactList.vue` | `views/crm/customer/ContactList.vue` | 联系人列表子组件 | | `crm/customer/components/contactForm.vue` | `views/crm/customer/ContactFormDialog.vue` | 联系人表单弹窗 | > **不迁移**:`components/InvoiceInfo.vue`(开票信息)、`statistics/index.vue`(统计分析)、`tag/index.vue`(标签管理)。 ### 1.4 公海池 | CRM 源文件 | 山峡2.0 目标位置 | 说明 | |---|---|---| | `crm/pool/index.vue` | `views/crm/customer/pool/index.vue` | 公海池列表,含统计+领取+分配 | | `crm/pool/components/claimDialog.vue` | `views/crm/customer/pool/ClaimDialog.vue` | **新增文件**,认领弹窗 | | `crm/pool/components/assignDialog.vue` | `views/crm/customer/pool/AssignDialog.vue` | 分配弹窗 | | — | `views/crm/customer/pool/RuleManager.vue` | 规则管理页,从 CRM 对应文件拷贝替换 | --- ## 2. 通用替换规则(所有文件必做) 以下替换为 **全局机械替换**,适用于所有迁移文件。 ### 2.1 移除 `getCurrentInstance` 和 `proxy` **CRM 写法**: ```vue ``` **替换为**: ```vue ``` ### 2.2 字典获取方式 **CRM 写法**: ```ts import { useDict } from '@/utils/dict'; const { crm_customer_type, crm_customer_level } = toRefs(proxy?.useDict('crm_customer_type', 'crm_customer_level')); ``` **替换为**(山峡2.0 有 `useDictTag` composable,**推荐使用**): ```ts import { useDictTag } from '@/composables/useDictTag' // 为每种字典创建一个 composable 实例 const { getLabel: getCustomerTypeLabel, getTagType: getCustomerTypeTagType } = useDictTag('crm_customer_type') const { getLabel: getCustomerLevelLabel, getTagType: getCustomerLevelTagType } = useDictTag('crm_customer_level') ``` 或直接使用 dictStore: ```ts import { useDictStore } from '@/store/modules/dict' const dictStore = useDictStore() // dictStore.getLabel(dictType, value) 获取标签文本 // dictStore.getTagType(dictType, value) 获取 tag 语义色 // dictStore.getOptions(dictType) 获取选项列表(用于下拉) ``` > **`useDictTag` composable** 位于 `src/composables/useDictTag.ts`,封装了 `getLabel`、`getTagType`、`options` 三个方法。 > **dictStore** 位于 `src/store/modules/dict.ts`,字典数据通过 `fetchAllDictData()` 一次性加载,每个字典项包含 `label`、`value`、`tagType` 三个字段。 ### 2.3 `` → `` **CRM 写法**: ```vue ``` **替换为**(使用 `useDictTag`): ```vue {{ getCustomerTypeLabel(scope.row.customerType) }} ``` > **不需要手写 `getXxxTagType` 映射函数**——山峡2.0 的 `useDictTag` 已内置 `getTagType`, > tagType 由后端字典数据中的 `tagType` 字段决定(数据库 `sys_dict_data` 表的 `css_class` 字段)。 Tag 语义色规则(来自工程规范): | 状态含义 | el-tag type | |---|---| | 正常/启用/已完成/通过 | `success` | | 进行中/待确认/已过期 | `warning` | | 异常/已作废/拒绝 | `danger` | | 待执行/待生效/停用/默认 | `info` | | 特殊分类/来源类型 | `primary`(或不设 type,使用默认绿色) | ### 2.4 消息提示 | CRM 写法 | 替换为 | |---|---| | `proxy?.$modal.msgSuccess('xxx')` | `ElMessage.success('xxx')` | | `proxy?.$modal.msgWarning('xxx')` | `ElMessage.warning('xxx')` | | `proxy?.$modal.msgError('xxx')` | `ElMessage.error('xxx')` | | `await proxy?.$modal.confirm('xxx')` | `await ElMessageBox.confirm('xxx', '提示', { type: 'warning' })` | **需要添加 import**: ```ts import { ElMessage, ElMessageBox } from 'element-plus' ``` ### 2.5 移除 RuoYi 工具导入 | CRM 导入 | 处理方式 | |---|---| | `import { addDateRange } from '@/utils/ruoyi'` | 删除。日期范围参数改为手动拼接到查询参数 | | `import { parseTime } from '@/utils/ruoyi'` | 用 `dayjs` 或原生 `Date` 格式化替代 | | `import { hasPermi } from '@/utils/permission'` | 删除或替换为山峡2.0 权限方案(见第 11 节) | | `import { scrollTo } from '@/utils/scroll-to'` | 删除(山峡2.0 不需要) | ### 2.6 `el-button` 图标 **CRM 写法**: ```vue 新增 ``` **替换为**(山峡2.0 要求显式 ``): ```vue 新增 ``` > icon 属性名如 `"Plus"` → `import { Plus } from '@element-plus/icons-vue'` 后用 `` 包裹。 ### 2.7 移除搜索动画 **CRM 写法**: ```vue
``` **替换为**(移除 transition 包裹,山峡2.0 不使用该动画): ```vue ``` 或保留 `v-show` 但去掉 `transition`: ```vue
``` --- ## 3. 页面外层布局替换 这是改造量最大的一块。CRM 的页面结构和山峡2.0 完全不同。 ### 3.1 CRM 原始结构 ```vue ``` ### 3.2 山峡2.0 目标结构 ```vue ``` ### 3.3 结构转换对照表 | CRM 元素 | 山峡2.0 对应 | 说明 | |---|---|---| | `
` | `
` | 最外层容器 | | `` 包裹搜索 | `
` | 筛选区,无 el-card 包裹 | | `` | `` | label-position 默认,不加 label-width | | `/` 栅格 | `
` 内的 `` | 自动网格布局 | | 搜索/重置按钮 `` | `
` | 操作按钮行 | | `` 内容区 | `
` 内直接放内容 | 无 el-card 包裹 | | `module-title` 标题 | `

标题

` | 标题栏 | | `` 工具栏 | `
` | 工具栏区域 | | `` 组件 | 删除 | 山峡2.0 不使用,工具栏按钮手动排列 | | `` | `` | 加 list-table class | | `` 组件 | `` 原生组件 | 见第 8 节 | --- ## 4. 弹窗/抽屉替换规则 ### 4.1 客户详情:`el-drawer` → `el-dialog` **CRM 写法**(`detail.vue`): ```vue
``` **替换为**: ```vue ``` > **关键变更**: > - `el-drawer` → `el-dialog`,尺寸从 `80%` 改为固定 `960px` > - 移除 `direction="rtl"` > - 左右两栏布局可保留但调整比例 > - 加 `style="backdrop-filter: none"` 禁止毛玻璃(工程规范要求) ### 4.2 表单弹窗 **CRM 写法**(`form.vue` 等): ```vue ``` **替换为**: ```vue ``` > **变更点**: > - `dialog.visible` → `innerVisible`(统一命名) > - 加 `destroy-on-close`、`align-center`、`close-on-click-modal="false"` > - 加 `style="backdrop-filter: none"` > - 加 `class="dialog-form-layout"` > - `append-to-body` 可保留(嵌套弹窗场景需要) ### 4.3 弹窗宽度参考 | 弹窗类型 | CRM 宽度 | 山峡2.0 建议宽度 | |---|---|---| | 新增/编辑表单 | 960px | 960px | | 客户详情 | drawer 80% | 960px(el-dialog) | | 转移弹窗 | 600px | 600px | | 成交弹窗 | 600px | 600px | | 放入公海弹窗 | 600px | 600px | | 跟进记录弹窗 | 1200px | 960px(缩小) | | 联系人表单 | 800px | 800px | | 列设置弹窗 | 600px | 600px | --- ## 5. 表单布局替换规则 ### 5.1 CRM 表单布局 ```vue 基本信息 ``` ### 5.2 山峡2.0 表单布局 ```vue
``` ### 5.3 表单转换对照 | CRM | 山峡2.0 | 说明 | |---|---|---| | `label-width="120px"` | `label-position="top"` | 标签在上方,不设固定宽度 | | `` | `
` | 自动两列网格 | | `` 分组 | `

` 或标题文字 | 可保留 divider 作为视觉分隔 | | `form` 变量名 | `formData` | 统一命名 | | `rules` 变量名 | `formRules` | 统一命名 | | 全宽字段 `` | `class="form-col-span-2"` | 跨两列 | > **注意**:CRM 的 `form.vue` 使用 `el-divider` 分区(基本信息/详细信息/附加信息),这个分区方式可以保留作为视觉分隔,但外层的 `el-row/el-col` 改为 `form-grid-2`。 --- ## 6. 图片/附件上传替换规则 ### 6.1 CRM 的图片组件 CRM 使用两个自定义组件: **ImageUpload**(`@/components/ImageUpload/index.vue`): - Props: `modelValue`(ossId 逗号分隔的字符串)、`limit`、`fileType`、`disabled` - 功能:上传图片到 OSS,返回 ossId 字符串 - 使用场景:客户表单中的营业执照、经营许可证、其他证件 **ImagePreview**(`@/components/ImagePreview/index.vue`): - Props: `src`(ossId)、`width`、`height`、`isOssId` - 功能:根据 ossId 获取图片 URL 并预览 - 使用场景:详情页、跟进记录附件展示 ### 6.2 山峡2.0 的附件方案 山峡2.0 使用 `fetchUploadAttachment` API 上传文件: ```ts import { fetchUploadAttachment } from '@/api/admin' // 上传文件 const formData = new FormData() formData.append('file', file) const result = await fetchUploadAttachment(formData) // result.fileUrl 为可访问的文件 URL ``` ### 6.3 替换方案 **场景 A:图片上传(表单中)** CRM 写法: ```vue ``` 替换为 — 使用原生 `el-upload` + `fetchUploadAttachment`: ```vue ``` ```ts async function handleFileUpload(uploadFile: UploadFile, field: string) { if (!uploadFile.raw) return const formData = new FormData() formData.append('file', uploadFile.raw) const result = await fetchUploadAttachment(formData) // 将 result.fileUrl 追加到 form[field] } ``` **场景 B:图片预览(详情页)** CRM 写法: ```vue ``` 替换为 — 直接用 `el-image`: ```vue ``` **场景 C:附件上传(跟进记录,支持多文件类型)** CRM 写法: ```vue ``` 替换为 — 使用 `el-upload` 的 `http-request` 自定义上传: ```vue 选择文件 ``` ```ts async function handleCustomUpload(options: UploadRequestOptions) { const formData = new FormData() formData.append('file', options.file) const result = await fetchUploadAttachment(formData) options.onSuccess(result) } ``` ### 6.4 图片存储字段差异 | CRM | 山峡2.0 | 说明 | |---|---|---| | `ossId`(逗号分隔的 OSS ID) | `fileUrl`(完整 URL) | CRM 通过 ossId 调 API 获取 URL,山峡2.0 直接存 URL | | `listByIds(ossIds)` 获取 URL 列表 | 不需要,URL 直接可用 | 无需额外请求 | --- ## 7. 字典/状态展示替换规则 详见第 2.3 节。这里补充完整的使用场景: ### 7.1 下拉选择中的字典 **CRM 写法**: ```vue ``` **替换为**: ```vue ``` ### 7.2 表格中的字典展示 **CRM 写法**: ```vue ``` **替换为**: ```vue ``` ### 7.3 详情页中的字典展示 **CRM 写法**: ```vue ``` **替换为**: ```vue {{ dictStore.getDictLabel('crm_customer_type', customerInfo.customerType) }} ``` ### 7.4 涉及的字典类型清单 | 字典编码 | 说明 | 使用模块 | |---|---|---| | `crm_customer_type` | 客户类型 | 客户管理 | | `crm_customer_level` | 客户级别 | 客户管理、公海池 | | `crm_clue_source` | 客户来源 | 客户管理、公海池 | | `crm_customer_status` | 客户状态 | 客户管理、公海池 | | `crm_deal_status` | 成交状态 | 客户管理、公海池 | | `crm_industry` | 所属行业 | 客户管理、公海池 | | `crm_customer_park` | 所在园区 | 客户管理、公海池 | | `crm_company_scale` | 企业规模 | 客户管理、公海池 | | `crm_follow_type` | 跟进方式 | 客户管理(跟进记录) | | `crm_contact_role` | 联系人角色 | 客户管理(联系人) | | `crm_product_status` | 产品状态 | 产品管理 | | `crm_product_unit` | 产品单位 | 产品管理 | | `sys_normal_disable` | 启用/停用 | 产品分类、公海池规则 | --- ## 8. 分页组件替换规则 ### 8.1 CRM 写法 ```vue const total = ref(0) const queryParams = reactive({ pageNum: 1, pageSize: 10, // ... }) ``` ### 8.2 山峡2.0 写法 ```vue const pagination = reactive({ page: 1, pageSize: 10, total: 0 }) ``` ### 8.3 数据结构对照 | CRM | 山峡2.0 | |---|---| | `queryParams.pageNum` | `pagination.page` | | `queryParams.pageSize` | `pagination.pageSize` | | `total` (ref) | `pagination.total` | | `@pagination` 事件 | `@size-change` + `@current-change` | ### 8.4 API 请求参数对照 | CRM 参数名 | 山峡2.0 参数名 | 说明 | |---|---|---| | `pageNum` | `page` | 页码 | | `pageSize` | `pageSize` | 每页条数(不变) | --- ## 9. 工具栏与操作按钮替换规则 ### 9.1 移除 `` CRM 在每个列表页工具栏末尾使用 ``,山峡2.0 不使用。 **CRM 写法**: ```vue 新增 修改 ``` **替换为**: ```vue
已选 {{ selectedRows.length }} 项
新增
``` ### 9.2 按钮 class 规范 | 场景 | class | type | |---|---|---| | 主操作(仅一个) | `btn-main` | `primary` | | 次要操作 | `btn-ghost` | 无 | | 危险操作 | 无 | `danger` | ### 9.3 视图切换 CRM 客户管理使用 `el-radio-group` + `el-radio-button` 切换视图: ```vue 我的客户 全部客户 待跟进 重点客户 ``` > **保留此组件**。`el-radio-group` + `el-radio-button` 是 Element Plus 原生组件,无需替换。放在 `panel-top` 标题下方即可。 --- ## 10. 消息提示/确认框替换规则 详见第 2.4 节。补充完整替换表: | CRM 方法 | 山峡2.0 替换 | |---|---| | `proxy?.$modal.msgSuccess(msg)` | `ElMessage.success(msg)` | | `proxy?.$modal.msgWarning(msg)` | `ElMessage.warning(msg)` | | `proxy?.$modal.msgError(msg)` | `ElMessage.error(msg)` | | `proxy?.$modal.confirm(msg)` | `ElMessageBox.confirm(msg, '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' })` | | `proxy?.$modal.confirm(msg).then(() => {...})` | `ElMessageBox.confirm(msg, '提示', { type: 'warning' }).then(() => {...})` | | `proxy?.$modal.alert(msg)` | `ElMessageBox.alert(msg, '提示')` | --- ## 11. 权限指令替换规则 ### 11.1 CRM 权限方式 ```ts import { hasPermi } from '@/utils/permission' // 模板中 新增 ``` ### 11.2 山峡2.0 权限方式 山峡2.0 使用 `v-auth` 指令(`src/directives/core/auth.ts`),基于当前路由 `meta.authList` 中的权限标识: ```vue 新增 编辑 删除 ``` > **注意**:`v-auth` 的权限标识来自路由 `meta.authList[].authMark`,不是 CRM 的 `crm:customer:add` 格式。 > 需要在路由配置中正确设置 `authList`。 ### 11.3 替换规则 | CRM 写法 | 山峡2.0 写法 | |---|---| | `v-if="hasPermi(['crm:customer:add'])"` | `v-auth="'add'"` | | `v-if="hasPermi(['crm:customer:edit'])"` | `v-auth="'edit'"` | | `v-if="hasPermi(['crm:customer:remove'])"` | `v-auth="'delete'"` | | `v-if="hasPermi(['crm:customer:export'])"` | `v-auth="'export'"` | | `v-if="hasPermi(['crm:customer:import'])"` | `v-auth="'import'"` | > 如果路由的 `authList` 暂未配置完整,可以**先移除权限判断**(删除 `v-if` / `v-auth`),后续统一补上。 --- ## 12. API 对接规则 ### 12.1 API 文件结构 CRM 前端 API 文件位于 `shanxia/crm/web/src/api/crm/`,山峡2.0 位于 `shanxia2.0/web-admin/src/api/crm/`。 **需确认**:山峡2.0 后端(Python FastAPI)的 API 路径和返回格式是否已经就绪。 ### 12.2 请求方式对照 | CRM 写法 | 山峡2.0 写法 | |---|---| | `import request from '@/utils/request'` | `import request from '@/api/request'` 或 `import { ... } from '@/api/crm/xxx'` | | `request({ url: '/crm/product/list', method: 'get', params: query })` | 需确认山峡2.0 的 request 封装格式 | ### 12.3 返回格式对照 **CRM(RuoYi)返回格式**: ```json { "code": 200, "msg": "操作成功", "rows": [...], "total": 100 } ``` **山峡2.0 返回格式**(需确认,可能为): ```json { "code": 200, "data": { "list": [...], "total": 100 } } ``` > ⚠️ **关键**:API 返回格式的字段名(`rows` vs `list`,顶层 vs `data` 包裹)需要逐个确认。这是最容易出 bug 的地方。 ### 12.4 类型定义 CRM 前端有独立的类型文件(如 `api/crm/customer/types.ts`、`api/crm/product/index.ts` 内定义的 interface)。拷贝代码时需要: 1. 将 CRM 的类型定义拷贝到山峡2.0 对应 API 文件中 2. 或在山峡2.0 的 API 文件中重新定义(字段名可能因后端不同而有差异) --- ## 13. 各模块特殊注意事项 ### 13.1 产品分类 - CRM 使用 `el-table` 的 `:tree-props` 展示树形结构,**保留** - CRM 有展开/折叠按钮(`toggleExpandAll`),**保留** - CRM 使用 `el-dialog` 内联新增/编辑表单,**保留** - 分类名称输入框使用 `check-strictly` 的 `el-tree-select`,**保留** - 字典只用到 `sys_normal_disable`(启用/停用) ### 13.2 产品管理 - CRM 内联了表单弹窗 + 详情弹窗 + 导入功能(849行),**保留内联** - CRM 使用 `ImageUpload` 上传产品图片,**需替换**(见第 6 节) - CRM 使用 `ImagePreview` 展示产品图片,**需替换** - CRM 有快速检索框 + 列设置功能,**保留** - CRM 导入使用原生 `el-upload` + `action` URL,**需改为山峡2.0 的上传方式** - CRM 详情使用内联 `el-dialog` 展示 `el-descriptions`,**保留** ### 13.3 客户管理 - **视图切换**(我的客户/全部/待跟进/重点):CRM 使用 `el-radio-group` + `el-radio-button`,**保留** - **快速检索**:CRM 在工具栏中有独立检索输入框,**保留** - **列设置**:CRM 有自定义的列显示/隐藏配置弹窗(`columnSettingDialog`),**保留** - **客户详情**:CRM 使用 `el-drawer` 80% 宽度,**改为 el-dialog 960px**(见第 4.1 节) - **详情内部布局**:CRM 用左右分栏(`el-col :span="8"` + `el-col :span="16"`),改为 `el-dialog` 后可改为上下布局或保持左右但缩小比例 - **详情 Tab 页**:CRM 有"跟进记录"、"联系人"、"操作日志" tab,**全部保留** - **跟进记录**: - CRM 有附件上传功能(`el-upload` + `ImagePreview`),**需替换上传方式** - CRM 使用 `el-timeline` 展示跟进记录,**保留** - **联系人**:CRM 使用独立的 `contactList.vue` + `contactForm.vue`,**保留** - **发票信息**:CRM 有独立的 `InvoiceInfo.vue`,**本次不迁移** ### 13.4 公海池 - CRM 有统计信息(`el-statistic` 展示公海池总数、今日新增等),**保留** - CRM 有认领功能(`claimDialog.vue`),**迁移**(山峡2.0 目前没有认领弹窗) - CRM 有分配功能(`assignDialog.vue`),山峡2.0 已有,**用 CRM 版本替换** - CRM 大量使用 `dict-tag`,**全部替换为 el-tag**(见第 7 节) - CRM 公海池规则管理是独立页面(不在本次 4 个模块内),山峡2.0 已有 `RuleManager.vue`,**用 CRM 版本替换** ### 13.5 公共 CSS 注意事项 CRM 使用的自定义 CSS class 需要检查是否需要迁移: | CRM CSS class | 说明 | 处理 | |---|---|---| | `.module-title` | 模块标题样式 | 山峡2.0 用 `.panel-top > h2`,删除 | | `.module-title-bar` | 标题前竖线 | 山峡2.0 用 `::before` 伪元素,自动生效 | | `.quick-search-input` | 快速检索框样式 | 保留,需拷贝对应 CSS | | `.column-setting-content` | 列设置弹窗样式 | 保留,需拷贝对应 CSS | | `.upload-thumb-image` | 上传缩略图 | 替换为山峡2.0 的图片展示方式 | | `.attachment-section` | 附件展示区域 | 保留,需拷贝对应 CSS | | `.upload-file-card` | 上传文件卡片 | 保留,需拷贝对应 CSS | | `.certificate-images` | 证件图片区域 | 保留,需拷贝对应 CSS | | `.preview-error` | 图片加载失败样式 | 保留,需拷贝对应 CSS | > CRM 文件 `