Files
shanxia2.0/docs/crm-frontend-migration-guide.md
T
2026-06-01 08:21:21 +08:00

35 KiB
Raw Blame History

CRM 前端代码迁移替换指南

策略:拷贝 CRM 原版 Vue 代码 → 机械替换框架差异 → 适配山峡2.0 设计体系 源系统:山峡 CRM(RuoYi-Vue-Plus,shanxia/crm/web/) 目标系统:山峡一体化门户(shanxia2.0/web-admin/) 版本:v1.0 | 2026-05-30


目录


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) 获取选项列表(用于下拉)

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 写法:

<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 返回格式的字段名(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 的导入导出

// 导出
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 启动验证页面渲染