1045 lines
35 KiB
Markdown
1045 lines
35 KiB
Markdown
# 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
|
||
<script setup lang="ts">
|
||
import { ref, reactive, getCurrentInstance, toRefs } from 'vue';
|
||
const { proxy } = getCurrentInstance() as any;
|
||
const { crm_customer_type } = toRefs<any>(proxy?.useDict('crm_customer_type'));
|
||
</script>
|
||
```
|
||
|
||
**替换为**:
|
||
```vue
|
||
<script setup lang="ts">
|
||
import { ref, reactive } from 'vue'
|
||
// 不需要 getCurrentInstance
|
||
// 不需要 proxy
|
||
</script>
|
||
```
|
||
|
||
### 2.2 字典获取方式
|
||
|
||
**CRM 写法**:
|
||
```ts
|
||
import { useDict } from '@/utils/dict';
|
||
const { crm_customer_type, crm_customer_level } = toRefs<any>(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 `<dict-tag>` → `<el-tag>`
|
||
|
||
**CRM 写法**:
|
||
```vue
|
||
<dict-tag :options="crm_customer_type" :value="scope.row.customerType" />
|
||
```
|
||
|
||
**替换为**(使用 `useDictTag`):
|
||
```vue
|
||
<el-tag :type="getCustomerTypeTagType(scope.row.customerType)" size="small">
|
||
{{ getCustomerTypeLabel(scope.row.customerType) }}
|
||
</el-tag>
|
||
```
|
||
|
||
> **不需要手写 `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
|
||
<el-button type="primary" icon="Plus" @click="handleAdd">新增</el-button>
|
||
```
|
||
|
||
**替换为**(山峡2.0 要求显式 `<el-icon>`):
|
||
```vue
|
||
<el-button type="primary" @click="handleAdd">
|
||
<el-icon><Plus /></el-icon>新增
|
||
</el-button>
|
||
```
|
||
|
||
> icon 属性名如 `"Plus"` → `import { Plus } from '@element-plus/icons-vue'` 后用 `<el-icon>` 包裹。
|
||
|
||
### 2.7 移除搜索动画
|
||
|
||
**CRM 写法**:
|
||
```vue
|
||
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
|
||
<div v-show="showSearch" class="mb-[10px]">
|
||
```
|
||
|
||
**替换为**(移除 transition 包裹,山峡2.0 不使用该动画):
|
||
```vue
|
||
<!-- 直接显示筛选区域,不使用过渡动画 -->
|
||
```
|
||
|
||
或保留 `v-show` 但去掉 `transition`:
|
||
```vue
|
||
<div v-show="showSearch">
|
||
```
|
||
|
||
---
|
||
|
||
## 3. 页面外层布局替换
|
||
|
||
这是改造量最大的一块。CRM 的页面结构和山峡2.0 完全不同。
|
||
|
||
### 3.1 CRM 原始结构
|
||
|
||
```vue
|
||
<template>
|
||
<div class="p-2">
|
||
<!-- 查询表单 -->
|
||
<transition ...>
|
||
<div v-show="showSearch" class="mb-[10px]">
|
||
<el-card shadow="hover">
|
||
<el-form ref="queryFormRef" :model="queryParams" label-width="90px">
|
||
<el-row :gutter="20">
|
||
<el-col :xl="4" :lg="6" :md="8" :sm="12" :xs="24">
|
||
<el-form-item label="客户名称" prop="customerName">
|
||
<el-input ... />
|
||
</el-form-item>
|
||
</el-col>
|
||
<!-- 更多筛选条件 -->
|
||
</el-row>
|
||
<el-row>
|
||
<el-col :span="24" style="text-align: right">
|
||
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
|
||
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
|
||
</el-col>
|
||
</el-row>
|
||
</el-form>
|
||
</el-card>
|
||
</div>
|
||
</transition>
|
||
|
||
<el-card shadow="never">
|
||
<template #header>
|
||
<div class="module-title mb-4">
|
||
<span class="module-title-bar"></span>
|
||
<span class="module-title-text">客户管理</span>
|
||
</div>
|
||
<!-- 工具栏按钮 -->
|
||
<el-row :gutter="10" class="mb8">
|
||
<el-col :span="4">...</el-col>
|
||
<el-col :span="1.5"><el-button ...>新增</el-button></el-col>
|
||
</el-row>
|
||
<right-toolbar ... />
|
||
</template>
|
||
<!-- 数据表格 -->
|
||
<el-table ... />
|
||
<!-- 分页 -->
|
||
<pagination ... />
|
||
</el-card>
|
||
</div>
|
||
</template>
|
||
```
|
||
|
||
### 3.2 山峡2.0 目标结构
|
||
|
||
```vue
|
||
<template>
|
||
<div class="page-container">
|
||
<section class="panel">
|
||
<!-- 标题栏 -->
|
||
<div class="panel-top">
|
||
<h2>客户管理</h2>
|
||
</div>
|
||
|
||
<!-- 筛选区 -->
|
||
<div class="filter-block">
|
||
<el-form :model="searchForm" class="filter-form" @submit.prevent>
|
||
<div class="filter-grid">
|
||
<el-form-item label="关键词">
|
||
<el-input v-model="searchForm.keyword" placeholder="客户名称/编号" clearable @keyup.enter="handleSearch" />
|
||
</el-form-item>
|
||
<!-- 更多筛选项 -->
|
||
</div>
|
||
<div class="filter-actions-row">
|
||
<span class="filter-hint">按客户名称、编号模糊查询。</span>
|
||
<div class="filter-actions-end">
|
||
<el-button class="btn-ghost" @click="handleReset">
|
||
<el-icon><RefreshLeft /></el-icon>重置
|
||
</el-button>
|
||
<el-button class="btn-main" type="primary" @click="handleSearch">
|
||
<el-icon><Search /></el-icon>搜索
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
</el-form>
|
||
</div>
|
||
|
||
<!-- 工具栏 -->
|
||
<div class="list-toolbar">
|
||
<div class="list-toolbar-row">
|
||
<div class="list-toolbar-selected">
|
||
已选 <span class="sel-num">{{ selectedRows.length }}</span> 项
|
||
</div>
|
||
<el-button class="btn-main" type="primary" @click="handleAdd">
|
||
<el-icon><Plus /></el-icon>新增客户
|
||
</el-button>
|
||
<!-- 更多按钮 -->
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 数据表格 -->
|
||
<div class="list-body list-wrap">
|
||
<el-table class="list-table" :data="tableData" v-loading="loading" stripe ...>
|
||
<!-- 表格列 -->
|
||
</el-table>
|
||
</div>
|
||
|
||
<!-- 分页 -->
|
||
<div class="pagination list-pager-bar">
|
||
<el-pagination
|
||
v-model:current-page="pagination.page"
|
||
v-model:page-size="pagination.pageSize"
|
||
:page-sizes="[10, 20, 30, 50]"
|
||
layout="total, sizes, prev, pager, next, jumper"
|
||
:total="pagination.total"
|
||
@size-change="handleSearch"
|
||
@current-change="handleSearch"
|
||
/>
|
||
</div>
|
||
</section>
|
||
</div>
|
||
</template>
|
||
```
|
||
|
||
### 3.3 结构转换对照表
|
||
|
||
| CRM 元素 | 山峡2.0 对应 | 说明 |
|
||
|---|---|---|
|
||
| `<div class="p-2">` | `<div class="page-container">` | 最外层容器 |
|
||
| `<el-card shadow="hover">` 包裹搜索 | `<div class="filter-block">` | 筛选区,无 el-card 包裹 |
|
||
| `<el-form label-width="90px">` | `<el-form class="filter-form">` | label-position 默认,不加 label-width |
|
||
| `<el-row>/<el-col>` 栅格 | `<div class="filter-grid">` 内的 `<el-form-item>` | 自动网格布局 |
|
||
| 搜索/重置按钮 `<el-col :span="24">` | `<div class="filter-actions-row">` | 操作按钮行 |
|
||
| `<el-card shadow="never">` 内容区 | `<section class="panel">` 内直接放内容 | 无 el-card 包裹 |
|
||
| `module-title` 标题 | `<div class="panel-top"><h2>标题</h2></div>` | 标题栏 |
|
||
| `<el-row :gutter="10">` 工具栏 | `<div class="list-toolbar">` | 工具栏区域 |
|
||
| `<right-toolbar>` 组件 | 删除 | 山峡2.0 不使用,工具栏按钮手动排列 |
|
||
| `<el-table>` | `<el-table class="list-table">` | 加 list-table class |
|
||
| `<pagination>` 组件 | `<el-pagination>` 原生组件 | 见第 8 节 |
|
||
|
||
---
|
||
|
||
## 4. 弹窗/抽屉替换规则
|
||
|
||
### 4.1 客户详情:`el-drawer` → `el-dialog`
|
||
|
||
**CRM 写法**(`detail.vue`):
|
||
```vue
|
||
<el-drawer v-model="visible" title="客户详情" direction="rtl" size="80%" destroy-on-close>
|
||
<div class="app-container">
|
||
<el-row :gutter="20">
|
||
<el-col :span="8"><!-- 左侧基本信息 --></el-col>
|
||
<el-col :span="16"><!-- 右侧 tab 区域 --></el-col>
|
||
</el-row>
|
||
</div>
|
||
</el-drawer>
|
||
```
|
||
|
||
**替换为**:
|
||
```vue
|
||
<el-dialog
|
||
v-model="innerVisible"
|
||
:title="detail.customerName || '客户详情'"
|
||
width="960px"
|
||
destroy-on-close
|
||
align-center
|
||
style="backdrop-filter: none"
|
||
class="dialog-form-layout"
|
||
>
|
||
<!-- 内部保持 el-tabs 结构不变 -->
|
||
</el-dialog>
|
||
```
|
||
|
||
> **关键变更**:
|
||
> - `el-drawer` → `el-dialog`,尺寸从 `80%` 改为固定 `960px`
|
||
> - 移除 `direction="rtl"`
|
||
> - 左右两栏布局可保留但调整比例
|
||
> - 加 `style="backdrop-filter: none"` 禁止毛玻璃(工程规范要求)
|
||
|
||
### 4.2 表单弹窗
|
||
|
||
**CRM 写法**(`form.vue` 等):
|
||
```vue
|
||
<el-dialog :title="dialog.title" v-model="dialog.visible" width="960px" append-to-body>
|
||
```
|
||
|
||
**替换为**:
|
||
```vue
|
||
<el-dialog
|
||
v-model="innerVisible"
|
||
:title="isEdit ? '编辑客户' : '新增客户'"
|
||
width="960px"
|
||
destroy-on-close
|
||
align-center
|
||
:close-on-click-modal="false"
|
||
style="backdrop-filter: none"
|
||
class="dialog-form-layout"
|
||
>
|
||
```
|
||
|
||
> **变更点**:
|
||
> - `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
|
||
<el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
|
||
<el-divider content-position="left">基本信息</el-divider>
|
||
<el-row :gutter="20">
|
||
<el-col :span="12">
|
||
<el-form-item label="客户名称" prop="customerName">
|
||
<el-input v-model="form.customerName" />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="客户简称" prop="shortName">
|
||
<el-input v-model="form.shortName" />
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
</el-form>
|
||
```
|
||
|
||
### 5.2 山峡2.0 表单布局
|
||
|
||
```vue
|
||
<el-form
|
||
ref="formRef"
|
||
:model="formData"
|
||
:rules="formRules"
|
||
label-position="top"
|
||
scroll-to-error
|
||
:scroll-into-view-options="{ behavior: 'smooth', block: 'center' }"
|
||
>
|
||
<div class="form-grid-2">
|
||
<el-form-item label="客户名称" prop="customerName">
|
||
<el-input v-model="formData.customerName" />
|
||
</el-form-item>
|
||
<el-form-item label="客户简称" prop="shortName">
|
||
<el-input v-model="formData.shortName" />
|
||
</el-form-item>
|
||
<!-- 全宽字段 -->
|
||
<el-form-item label="备注" class="form-col-span-2">
|
||
<el-input v-model="formData.remark" type="textarea" :rows="2" />
|
||
</el-form-item>
|
||
</div>
|
||
</el-form>
|
||
```
|
||
|
||
### 5.3 表单转换对照
|
||
|
||
| CRM | 山峡2.0 | 说明 |
|
||
|---|---|---|
|
||
| `label-width="120px"` | `label-position="top"` | 标签在上方,不设固定宽度 |
|
||
| `<el-row :gutter="20"><el-col :span="12">` | `<div class="form-grid-2">` | 自动两列网格 |
|
||
| `<el-divider>` 分组 | `<h3>` 或标题文字 | 可保留 divider 作为视觉分隔 |
|
||
| `form` 变量名 | `formData` | 统一命名 |
|
||
| `rules` 变量名 | `formRules` | 统一命名 |
|
||
| 全宽字段 `<el-col :span="24">` | `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
|
||
<ImageUpload v-model="form.businessLicenseImages" :limit="5" :file-type="['jpg', 'jpeg', 'png', 'pdf']" />
|
||
```
|
||
|
||
替换为 — 使用原生 `el-upload` + `fetchUploadAttachment`:
|
||
```vue
|
||
<el-upload
|
||
:file-list="licenseFileList"
|
||
list-type="picture-card"
|
||
:auto-upload="false"
|
||
:on-change="(file) => handleFileUpload(file, 'businessLicenseImages')"
|
||
accept=".jpg,.jpeg,.png,.pdf"
|
||
:limit="5"
|
||
>
|
||
<el-icon><Plus /></el-icon>
|
||
</el-upload>
|
||
```
|
||
|
||
```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
|
||
<ImagePreview :src="file.ossId" :isOssId="true" class="upload-thumb-image" />
|
||
```
|
||
|
||
替换为 — 直接用 `el-image`:
|
||
```vue
|
||
<el-image
|
||
:src="file.url || file.fileUrl"
|
||
fit="cover"
|
||
:preview-src-list="[file.url || file.fileUrl]"
|
||
preview-teleported
|
||
style="width: 100px; height: 100px"
|
||
/>
|
||
```
|
||
|
||
**场景 C:附件上传(跟进记录,支持多文件类型)**
|
||
|
||
CRM 写法:
|
||
```vue
|
||
<el-upload drag list-type="picture-card" :action="uploadAction" :headers="uploadHeaders" ...>
|
||
```
|
||
|
||
替换为 — 使用 `el-upload` 的 `http-request` 自定义上传:
|
||
```vue
|
||
<el-upload
|
||
:file-list="attachmentList"
|
||
:http-request="handleCustomUpload"
|
||
:on-remove="handleRemoveAttachment"
|
||
:on-preview="handlePreviewFile"
|
||
:limit="15"
|
||
multiple
|
||
accept="image/*,video/*,.pdf,.doc,.docx,.txt"
|
||
>
|
||
<el-button type="primary">
|
||
<el-icon><Upload /></el-icon>选择文件
|
||
</el-button>
|
||
<template #tip>
|
||
<div class="el-upload__tip">支持图片、视频、PDF、Word等格式,最多15个文件。</div>
|
||
</template>
|
||
</el-upload>
|
||
```
|
||
|
||
```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
|
||
<el-select v-model="queryParams.customerType">
|
||
<el-option v-for="dict in crm_customer_type" :key="dict.value" :label="dict.label" :value="dict.value" />
|
||
</el-select>
|
||
```
|
||
|
||
**替换为**:
|
||
```vue
|
||
<el-select v-model="searchForm.customerType" clearable>
|
||
<el-option
|
||
v-for="opt in dictStore.getDictItems('crm_customer_type')"
|
||
:key="opt.value"
|
||
:value="opt.value"
|
||
:label="opt.label"
|
||
/>
|
||
</el-select>
|
||
```
|
||
|
||
### 7.2 表格中的字典展示
|
||
|
||
**CRM 写法**:
|
||
```vue
|
||
<el-table-column label="客户类型" prop="customerType">
|
||
<template #default="scope">
|
||
<dict-tag :options="crm_customer_type" :value="scope.row.customerType" />
|
||
</template>
|
||
</el-table-column>
|
||
```
|
||
|
||
**替换为**:
|
||
```vue
|
||
<el-table-column label="客户类型" prop="customerType" min-width="100" align="center" header-align="center">
|
||
<template #default="{ row }">
|
||
<el-tag :type="getTagType('crm_customer_type', row.customerType)" size="small">
|
||
{{ dictStore.getDictLabel('crm_customer_type', row.customerType) }}
|
||
</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
```
|
||
|
||
### 7.3 详情页中的字典展示
|
||
|
||
**CRM 写法**:
|
||
```vue
|
||
<el-descriptions-item label="客户类型">
|
||
<dict-tag :options="crm_customer_type" :value="customerInfo.customerType" />
|
||
</el-descriptions-item>
|
||
```
|
||
|
||
**替换为**:
|
||
```vue
|
||
<el-descriptions-item label="客户类型">
|
||
<el-tag :type="getTagType('crm_customer_type', customerInfo.customerType)" size="small">
|
||
{{ dictStore.getDictLabel('crm_customer_type', customerInfo.customerType) }}
|
||
</el-tag>
|
||
</el-descriptions-item>
|
||
```
|
||
|
||
### 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
|
||
<!-- 模板中 -->
|
||
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
|
||
|
||
<!-- script 中 -->
|
||
const total = ref(0)
|
||
const queryParams = reactive({
|
||
pageNum: 1,
|
||
pageSize: 10,
|
||
// ...
|
||
})
|
||
```
|
||
|
||
### 8.2 山峡2.0 写法
|
||
|
||
```vue
|
||
<!-- 模板中 -->
|
||
<div class="pagination list-pager-bar">
|
||
<el-pagination
|
||
v-model:current-page="pagination.page"
|
||
v-model:page-size="pagination.pageSize"
|
||
:page-sizes="[10, 20, 30, 50]"
|
||
layout="total, sizes, prev, pager, next, jumper"
|
||
:total="pagination.total"
|
||
@size-change="handleSearch"
|
||
@current-change="handleSearch"
|
||
/>
|
||
</div>
|
||
|
||
<!-- script 中 -->
|
||
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 移除 `<right-toolbar>`
|
||
|
||
CRM 在每个列表页工具栏末尾使用 `<right-toolbar>`,山峡2.0 不使用。
|
||
|
||
**CRM 写法**:
|
||
```vue
|
||
<el-row :gutter="10" class="mb8">
|
||
<el-col :span="1.5"><el-button ...>新增</el-button></el-col>
|
||
<el-col :span="1.5"><el-button ...>修改</el-button></el-col>
|
||
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList" />
|
||
</el-row>
|
||
```
|
||
|
||
**替换为**:
|
||
```vue
|
||
<div class="list-toolbar">
|
||
<div class="list-toolbar-row">
|
||
<div class="list-toolbar-selected">
|
||
已选 <span class="sel-num">{{ selectedRows.length }}</span> 项
|
||
</div>
|
||
<el-button class="btn-main" type="primary" @click="handleAdd">
|
||
<el-icon><Plus /></el-icon>新增
|
||
</el-button>
|
||
<!-- 更多按钮 -->
|
||
</div>
|
||
</div>
|
||
```
|
||
|
||
### 9.2 按钮 class 规范
|
||
|
||
| 场景 | class | type |
|
||
|---|---|---|
|
||
| 主操作(仅一个) | `btn-main` | `primary` |
|
||
| 次要操作 | `btn-ghost` | 无 |
|
||
| 危险操作 | 无 | `danger` |
|
||
|
||
### 9.3 视图切换
|
||
|
||
CRM 客户管理使用 `el-radio-group` + `el-radio-button` 切换视图:
|
||
|
||
```vue
|
||
<el-radio-group v-model="activeView" @change="handleViewChange">
|
||
<el-radio-button value="mine">我的客户</el-radio-button>
|
||
<el-radio-button value="all">全部客户</el-radio-button>
|
||
<el-radio-button value="toFollow">待跟进</el-radio-button>
|
||
<el-radio-button value="key">重点客户</el-radio-button>
|
||
</el-radio-group>
|
||
```
|
||
|
||
> **保留此组件**。`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'
|
||
// 模板中
|
||
<el-button v-if="hasPermi(['crm:customer:add'])" ...>新增</el-button>
|
||
```
|
||
|
||
### 11.2 山峡2.0 权限方式
|
||
|
||
山峡2.0 使用 `v-auth` 指令(`src/directives/core/auth.ts`),基于当前路由 `meta.authList` 中的权限标识:
|
||
|
||
```vue
|
||
<!-- 模板中,直接用 v-auth 指令 -->
|
||
<el-button v-auth="'add'">新增</el-button>
|
||
<el-button v-auth="'edit'">编辑</el-button>
|
||
<el-button v-auth="'delete'">删除</el-button>
|
||
```
|
||
|
||
> **注意**:`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 文件 `<style>` 部分的自定义 CSS **需要一并拷贝**到山峡2.0 对应文件中。
|
||
|
||
---
|
||
|
||
## 附录 A:导入/导出替换
|
||
|
||
### CRM 的导入导出
|
||
|
||
```ts
|
||
// 导出
|
||
import { exportProduct } from '@/api/crm/product'
|
||
const blob = await exportProduct(queryParams)
|
||
// RuoYi 内部处理下载
|
||
|
||
// 导入
|
||
import { importTemplate } from '@/api/crm/product'
|
||
// 使用 el-upload + action URL
|
||
```
|
||
|
||
### 山峡2.0 的导入导出
|
||
|
||
```ts
|
||
// 导出 - 手动下载
|
||
const res = await request.post('/api/admin/crm/product/export', searchForm, { responseType: 'blob' })
|
||
const url = URL.createObjectURL(new Blob([res]))
|
||
const link = document.createElement('a')
|
||
link.href = url
|
||
link.download = '产品列表.xlsx'
|
||
link.click()
|
||
|
||
// 导入 - 使用 el-upload 的 on-change
|
||
const uploadRef = ref()
|
||
async function handleUploadChange(file: UploadFile) {
|
||
if (!file.raw) return
|
||
const formData = new FormData()
|
||
formData.append('file', file.raw)
|
||
await request.post('/api/admin/crm/product/import', formData)
|
||
ElMessage.success('导入成功')
|
||
handleSearch()
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 附录 B:执行检查清单
|
||
|
||
每个文件拷贝后,按此清单逐项检查:
|
||
|
||
- [ ] 移除 `getCurrentInstance` 和 `proxy` 引用
|
||
- [ ] 替换 `proxy?.useDict()` → `useDictStore()`
|
||
- [ ] 替换所有 `<dict-tag>` → `<el-tag>`
|
||
- [ ] 替换 `proxy?.$modal.msg*` → `ElMessage.*`
|
||
- [ ] 替换 `proxy?.$modal.confirm` → `ElMessageBox.confirm`
|
||
- [ ] 替换 `el-button icon="Xxx"` → `<el-icon><Xxx /></el-icon>`
|
||
- [ ] 移除 `<right-toolbar>`
|
||
- [ ] 移除 `<transition>` 搜索动画
|
||
- [ ] 移除 RuoYi 工具导入(`addDateRange`, `parseTime`, `hasPermi`)
|
||
- [ ] 外层布局改为 `.page-container` > `.panel` 结构
|
||
- [ ] 筛选区改为 `.filter-block` + `.filter-grid`
|
||
- [ ] 工具栏改为 `.list-toolbar`
|
||
- [ ] 弹窗加 `backdrop-filter: none` + `class="dialog-form-layout"`
|
||
- [ ] `el-drawer` → `el-dialog`
|
||
- [ ] 表单加 `label-position="top"`,改为 `.form-grid-2`
|
||
- [ ] 替换 `<pagination>` → `<el-pagination>`
|
||
- [ ] 替换 `ImageUpload` / `ImagePreview` → 山峡2.0 上传方案
|
||
- [ ] API 路径和返回格式对齐山峡2.0 后端
|
||
- [ ] 拷贝 `<style>` 中的自定义 CSS
|
||
- [ ] `pnpm dev` 启动验证页面渲染
|