35 KiB
CRM 前端代码迁移替换指南
策略:拷贝 CRM 原版 Vue 代码 → 机械替换框架差异 → 适配山峡2.0 设计体系 源系统:山峡 CRM(RuoYi-Vue-Plus,
shanxia/crm/web/) 目标系统:山峡一体化门户(shanxia2.0/web-admin/) 版本:v1.0 | 2026-05-30
目录
- 1. 迁移模块清单与文件映射
- 2. 通用替换规则(所有文件必做)
- 3. 页面外层布局替换
- 4. 弹窗/抽屉替换规则
- 5. 表单布局替换规则
- 6. 图片/附件上传替换规则
- 7. 字典/状态展示替换规则
- 8. 分页组件替换规则
- 9. 工具栏与操作按钮替换规则
- 10. 消息提示/确认框替换规则
- 11. 权限指令替换规则
- 12. API 对接规则
- 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 写法:
<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>
替换为:
<script setup lang="ts">
import { ref, reactive } from 'vue'
// 不需要 getCurrentInstance
// 不需要 proxy
</script>
2.2 字典获取方式
CRM 写法:
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,推荐使用):
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:
import { useDictStore } from '@/store/modules/dict'
const dictStore = useDictStore()
// dictStore.getLabel(dictType, value) 获取标签文本
// dictStore.getTagType(dictType, value) 获取 tag 语义色
// dictStore.getOptions(dictType) 获取选项列表(用于下拉)
useDictTagcomposable 位于src/composables/useDictTag.ts,封装了getLabel、getTagType、options三个方法。 dictStore 位于src/store/modules/dict.ts,字典数据通过fetchAllDictData()一次性加载,每个字典项包含label、value、tagType三个字段。
2.3 <dict-tag> → <el-tag>
CRM 写法:
<dict-tag :options="crm_customer_type" :value="scope.row.customerType" />
替换为(使用 useDictTag):
<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:
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 写法:
<el-button type="primary" icon="Plus" @click="handleAdd">新增</el-button>
替换为(山峡2.0 要求显式 <el-icon>):
<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 写法:
<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 不使用该动画):
<!-- 直接显示筛选区域,不使用过渡动画 -->
或保留 v-show 但去掉 transition:
<div v-show="showSearch">
3. 页面外层布局替换
这是改造量最大的一块。CRM 的页面结构和山峡2.0 完全不同。
3.1 CRM 原始结构
<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 目标结构
<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):
<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>
替换为:
<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 等):
<el-dialog :title="dialog.title" v-model="dialog.visible" width="960px" append-to-body>
替换为:
<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 表单布局
<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 表单布局
<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 上传文件:
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 写法:
<ImageUpload v-model="form.businessLicenseImages" :limit="5" :file-type="['jpg', 'jpeg', 'png', 'pdf']" />
替换为 — 使用原生 el-upload + fetchUploadAttachment:
<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>
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 写法:
<ImagePreview :src="file.ossId" :isOssId="true" class="upload-thumb-image" />
替换为 — 直接用 el-image:
<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 写法:
<el-upload drag list-type="picture-card" :action="uploadAction" :headers="uploadHeaders" ...>
替换为 — 使用 el-upload 的 http-request 自定义上传:
<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>
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 写法:
<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>
替换为:
<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 写法:
<el-table-column label="客户类型" prop="customerType">
<template #default="scope">
<dict-tag :options="crm_customer_type" :value="scope.row.customerType" />
</template>
</el-table-column>
替换为:
<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 写法:
<el-descriptions-item label="客户类型">
<dict-tag :options="crm_customer_type" :value="customerInfo.customerType" />
</el-descriptions-item>
替换为:
<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 写法
<!-- 模板中 -->
<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 写法
<!-- 模板中 -->
<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 写法:
<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>
替换为:
<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 切换视图:
<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 权限方式
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 中的权限标识:
<!-- 模板中,直接用 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)返回格式:
{
"code": 200,
"msg": "操作成功",
"rows": [...],
"total": 100
}
山峡2.0 返回格式(需确认,可能为):
{
"code": 200,
"data": {
"list": [...],
"total": 100
}
}
⚠️ 关键:API 返回格式的字段名(
rowsvslist,顶层 vsdata包裹)需要逐个确认。这是最容易出 bug 的地方。
12.4 类型定义
CRM 前端有独立的类型文件(如 api/crm/customer/types.ts、api/crm/product/index.ts 内定义的 interface)。拷贝代码时需要:
- 将 CRM 的类型定义拷贝到山峡2.0 对应 API 文件中
- 或在山峡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+actionURL,需改为山峡2.0 的上传方式 - CRM 详情使用内联
el-dialog展示el-descriptions,保留
13.3 客户管理
- 视图切换(我的客户/全部/待跟进/重点):CRM 使用
el-radio-group+el-radio-button,保留 - 快速检索:CRM 在工具栏中有独立检索输入框,保留
- 列设置:CRM 有自定义的列显示/隐藏配置弹窗(
columnSettingDialog),保留 - 客户详情:CRM 使用
el-drawer80% 宽度,改为 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 有附件上传功能(
- 联系人: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 的导入导出
// 导出
import { exportProduct } from '@/api/crm/product'
const blob = await exportProduct(queryParams)
// RuoYi 内部处理下载
// 导入
import { importTemplate } from '@/api/crm/product'
// 使用 el-upload + action URL
山峡2.0 的导入导出
// 导出 - 手动下载
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启动验证页面渲染