ソースを参照

Merge remote-tracking branch 'yudao/dev' into crm-msg

dhb52 1 年間 前
コミット
e033793a94
40 ファイル変更1943 行追加608 行削除
  1. 1 3
      src/api/bpm/model/index.ts
  2. 25 0
      src/api/crm/bi/rank.ts
  3. 0 34
      src/api/crm/bi/ranking.ts
  4. 1 1
      src/api/crm/business/index.ts
  5. 6 0
      src/api/crm/clue/index.ts
  6. 1 1
      src/api/crm/contact/index.ts
  7. 6 4
      src/api/crm/contract/index.ts
  8. 1 1
      src/api/crm/customer/index.ts
  9. 50 0
      src/api/erp/product/category/index.ts
  10. 64 0
      src/api/erp/product/index.ts
  11. 44 0
      src/api/erp/product/unit/index.ts
  12. 1 0
      src/components/Table/src/TableSelectForm.vue
  13. 6 2
      src/store/modules/user.ts
  14. 1 1
      src/views/crm/backlog/index.vue
  15. 107 0
      src/views/crm/bi/rank/ContractPriceRank.vue
  16. 108 0
      src/views/crm/bi/rank/ReceivablePriceRank.vue
  17. 100 0
      src/views/crm/bi/rank/index.vue
  18. 0 133
      src/views/crm/bi/ranking/components/RankingContractStatistics.vue
  19. 0 133
      src/views/crm/bi/ranking/components/RankingReceivablesStatistics.vue
  20. 0 91
      src/views/crm/bi/ranking/index.vue
  21. 8 6
      src/views/crm/contact/detail/index.vue
  22. 1 1
      src/views/crm/contact/index.vue
  23. 83 83
      src/views/crm/contract/ContractForm.vue
  24. 0 31
      src/views/crm/contract/components/BPMLModel.vue
  25. 83 24
      src/views/crm/contract/components/ProductList.vue
  26. 12 7
      src/views/crm/contract/detail/ContractDetailsHeader.vue
  27. 32 2
      src/views/crm/contract/detail/ContractDetailsInfo.vue
  28. 67 0
      src/views/crm/contract/detail/ContractProductList.vue
  29. 25 17
      src/views/crm/contract/detail/index.vue
  30. 73 28
      src/views/crm/contract/index.vue
  31. 1 0
      src/views/crm/contract/oa/ContractDetail/index.vue
  32. 2 1
      src/views/crm/customer/CustomerImportForm.vue
  33. 1 1
      src/views/crm/customer/detail/index.vue
  34. 6 3
      src/views/crm/customer/index.vue
  35. 154 0
      src/views/erp/product/ProductForm.vue
  36. 145 0
      src/views/erp/product/category/ProductCategoryForm.vue
  37. 220 0
      src/views/erp/product/category/index.vue
  38. 208 0
      src/views/erp/product/index.vue
  39. 100 0
      src/views/erp/product/unit/ProductUnitForm.vue
  40. 200 0
      src/views/erp/product/unit/index.vue

+ 1 - 3
src/api/bpm/model/index.ts

@@ -32,9 +32,7 @@ export const getModelPage = async (params) => {
 export const getModel = async (id: number) => {
   return await request.get({ url: '/bpm/model/get?id=' + id })
 }
-export const getModelByKey = async (key: string) => {
-  return await request.get({ url: '/bpm/model/get-by-key?key=' + key })
-}
+
 export const updateModel = async (data: ModelVO) => {
   return await request.put({ url: '/bpm/model/update', data: data })
 }

+ 25 - 0
src/api/crm/bi/rank.ts

@@ -0,0 +1,25 @@
+import request from '@/config/axios'
+
+export interface BiRankRespVO {
+  count: number
+  nickname: string
+  deptName: string
+}
+
+// 排行 API
+export const RankApi = {
+  // 获得合同排行榜
+  getContractPriceRank: (params: any) => {
+    return request.get({
+      url: '/crm/bi-rank/get-contract-price-rank',
+      params
+    })
+  },
+  // 获得回款排行榜
+  getReceivablePriceRank: (params: any) => {
+    return request.get({
+      url: '/crm/bi-rank/get-receivable-price-rank',
+      params
+    })
+  }
+}

+ 0 - 34
src/api/crm/bi/ranking.ts

@@ -1,34 +0,0 @@
-import request from '@/config/axios'
-
-export interface BiContractRanKingRespVO {
-  price: number
-  nickname: string
-  deptName: string
-}
-export interface BiReceivablesRanKingRespVO {
-  price: number
-  nickname: string
-  deptName: string
-}
-export interface BiRankReqVO {
-  deptId: number
-  type: string
-}
-
-// 排行 API
-export const RankingStatisticsApi = {
-  // 获得合同排行榜
-  contractAmountRanking: (params: any) => {
-    return request.get({
-      url: '/bi/ranking/contract-ranking',
-      params
-    })
-  },
-  // 获得回款排行榜
-  receivablesAmountRanking: (params: any) => {
-    return request.get({
-      url: '/bi/ranking/receivables-ranking',
-      params
-    })
-  }
-}

+ 1 - 1
src/api/crm/business/index.ts

@@ -73,6 +73,6 @@ export const getBusinessListByIds = async (val: number[]) => {
 }
 
 // 商机转移
-export const transfer = async (data: TransferReqVO) => {
+export const transferBusiness = async (data: TransferReqVO) => {
   return await request.put({ url: '/crm/business/transfer', data })
 }

+ 6 - 0
src/api/crm/clue/index.ts

@@ -1,4 +1,5 @@
 import request from '@/config/axios'
+import { TransferReqVO } from '@/api/crm/customer'
 
 export interface ClueVO {
   id: number
@@ -44,3 +45,8 @@ export const deleteClue = async (id: number) => {
 export const exportClue = async (params) => {
   return await request.download({ url: `/crm/clue/export-excel`, params })
 }
+
+// 线索转移
+export const transferClue = async (data: TransferReqVO) => {
+  return await request.put({ url: '/crm/clue/transfer', data })
+}

+ 1 - 1
src/api/crm/contact/index.ts

@@ -88,6 +88,6 @@ export const deleteContactBusinessList = async (data: ContactBusinessReqVO) => {
 }
 
 // 联系人转移
-export const transfer = async (data: TransferReqVO) => {
+export const transferContact = async (data: TransferReqVO) => {
   return await request.put({ url: '/crm/contact/transfer', data })
 }

+ 6 - 4
src/api/crm/contract/index.ts

@@ -7,6 +7,7 @@ export interface ContractVO {
   name: string
   customerId: number
   businessId: number
+  businessName: string
   processInstanceId: number
   orderDate: Date
   ownerUserId: number
@@ -18,8 +19,9 @@ export interface ContractVO {
   productPrice: number
   contactId: number
   signUserId: number
+  signUserName: string
   contactLastTime: Date
-  status: number
+  auditStatus: number
   remark: string
   productItems: ProductExpandVO[]
   creatorName: string
@@ -66,11 +68,11 @@ export const exportContract = async (params) => {
 }
 
 // 提交审核
-export const handleApprove = async (id: number) => {
-  return await request.put({ url: `/crm/contract/approve?id=${id}` })
+export const submitContract = async (id: number) => {
+  return await request.put({ url: `/crm/contract/submit?id=${id}` })
 }
 
 // 合同转移
-export const transfer = async (data: TransferReqVO) => {
+export const transferContract = async (data: TransferReqVO) => {
   return await request.put({ url: '/crm/contract/transfer', data })
 }

+ 1 - 1
src/api/crm/customer/index.ts

@@ -82,7 +82,7 @@ export interface TransferReqVO {
 }
 
 // 客户转移
-export const transfer = async (data: TransferReqVO) => {
+export const transferCustomer = async (data: TransferReqVO) => {
   return await request.put({ url: '/crm/customer/transfer', data })
 }
 

+ 50 - 0
src/api/erp/product/category/index.ts

@@ -0,0 +1,50 @@
+import request from '@/config/axios'
+
+// ERP 商品分类 VO
+export interface ProductCategoryVO {
+  // 分类编号
+  id: number
+  // 父分类编号
+  parentId: number
+  // 分类名称
+  name: string
+  // 分类编码
+  code: string
+  // 分类排序
+  sort: number
+  // 开启状态
+  status: number
+}
+
+// ERP 商品分类 API
+export const ProductCategoryApi = {
+  // 查询ERP 商品分类列表
+  getProductCategoryList: async (params) => {
+    return await request.get({ url: `/erp/product-category/list`, params })
+  },
+
+  // 查询ERP 商品分类详情
+  getProductCategory: async (id: number) => {
+    return await request.get({ url: `/erp/product-category/get?id=` + id })
+  },
+
+  // 新增ERP 商品分类
+  createProductCategory: async (data: ProductCategoryVO) => {
+    return await request.post({ url: `/erp/product-category/create`, data })
+  },
+
+  // 修改ERP 商品分类
+  updateProductCategory: async (data: ProductCategoryVO) => {
+    return await request.put({ url: `/erp/product-category/update`, data })
+  },
+
+  // 删除ERP 商品分类
+  deleteProductCategory: async (id: number) => {
+    return await request.delete({ url: `/erp/product-category/delete?id=` + id })
+  },
+
+  // 导出ERP 商品分类 Excel
+  exportProductCategory: async (params) => {
+    return await request.download({ url: `/erp/product-category/export-excel`, params })
+  }
+}

+ 64 - 0
src/api/erp/product/index.ts

@@ -0,0 +1,64 @@
+import request from '@/config/axios'
+
+// ERP 产品 VO
+export interface ProductVO {
+  // 产品编号
+  id: number
+  // 产品名称
+  name: string
+  // 产品条码
+  barCode: string
+  // 产品类型编号
+  categoryId: number
+  // 单位编号
+  unitId: number
+  // 产品状态
+  status: number
+  // 产品规格
+  standard: string
+  // 产品备注
+  remark: string
+  // 保质期天数
+  expiryDay: number
+  // 基础重量(kg)
+  weight: number
+  // 采购价格,单位:元
+  purchasePrice: number
+  // 销售价格,单位:元
+  salePrice: number
+  // 最低价格,单位:元
+  minPrice: number
+}
+
+// ERP 产品 API
+export const ProductApi = {
+  // 查询ERP 产品分页
+  getProductPage: async (params: any) => {
+    return await request.get({ url: `/erp/product/page`, params })
+  },
+
+  // 查询ERP 产品详情
+  getProduct: async (id: number) => {
+    return await request.get({ url: `/erp/product/get?id=` + id })
+  },
+
+  // 新增ERP 产品
+  createProduct: async (data: ProductVO) => {
+    return await request.post({ url: `/erp/product/create`, data })
+  },
+
+  // 修改ERP 产品
+  updateProduct: async (data: ProductVO) => {
+    return await request.put({ url: `/erp/product/update`, data })
+  },
+
+  // 删除ERP 产品
+  deleteProduct: async (id: number) => {
+    return await request.delete({ url: `/erp/product/delete?id=` + id })
+  },
+
+  // 导出ERP 产品 Excel
+  exportProduct: async (params) => {
+    return await request.download({ url: `/erp/product/export-excel`, params })
+  }
+}

+ 44 - 0
src/api/erp/product/unit/index.ts

@@ -0,0 +1,44 @@
+import request from '@/config/axios'
+
+// ERP 产品单位 VO
+export interface ProductUnitVO {
+  // 单位编号
+  id: number
+  // 单位名字
+  name: string
+  // 单位状态
+  status: number
+}
+
+// ERP 产品单位 API
+export const ProductUnitApi = {
+  // 查询ERP 产品单位分页
+  getProductUnitPage: async (params: any) => {
+    return await request.get({ url: `/erp/product-unit/page`, params })
+  },
+
+  // 查询ERP 产品单位详情
+  getProductUnit: async (id: number) => {
+    return await request.get({ url: `/erp/product-unit/get?id=` + id })
+  },
+
+  // 新增ERP 产品单位
+  createProductUnit: async (data: ProductUnitVO) => {
+    return await request.post({ url: `/erp/product-unit/create`, data })
+  },
+
+  // 修改ERP 产品单位
+  updateProductUnit: async (data: ProductUnitVO) => {
+    return await request.put({ url: `/erp/product-unit/update`, data })
+  },
+
+  // 删除ERP 产品单位
+  deleteProductUnit: async (id: number) => {
+    return await request.delete({ url: `/erp/product-unit/delete?id=` + id })
+  },
+
+  // 导出ERP 产品单位 Excel
+  exportProductUnit: async (params) => {
+    return await request.download({ url: `/erp/product-unit/export-excel`, params })
+  }
+}

+ 1 - 0
src/components/Table/src/TableSelectForm.vue

@@ -1,3 +1,4 @@
+<!-- 列表选择通用组件,参考 ProductList 组件使用 -->
 <template>
   <Dialog v-model="dialogVisible" :appendToBody="true" :scroll="true" :title="title" width="60%">
     <el-table

+ 6 - 2
src/store/modules/user.ts

@@ -10,7 +10,9 @@ interface UserVO {
   id: number
   avatar: string
   nickname: string
+  deptId: number
 }
+
 interface UserInfoVO {
   permissions: string[]
   roles: string[]
@@ -26,7 +28,8 @@ export const useUserStore = defineStore('admin-user', {
     user: {
       id: 0,
       avatar: '',
-      nickname: ''
+      nickname: '',
+      deptId: 0
     }
   }),
   getters: {
@@ -73,7 +76,8 @@ export const useUserStore = defineStore('admin-user', {
       this.user = {
         id: 0,
         avatar: '',
-        nickname: ''
+        nickname: '',
+        deptId: 0
       }
     }
   }

+ 1 - 1
src/views/crm/backlog/index.vue

@@ -96,8 +96,8 @@ const leftSides = ref([
 const sideClick = (item) => {
   leftType.value = item.infoType
 }
+// TODO @dhb52: 侧边栏样式,在黑暗模式下,颜色会不对。是不是可以读取主题色哈;
 </script>
-
 <style lang="scss" scoped>
 .side-item-list {
   top: 0;

+ 107 - 0
src/views/crm/bi/rank/ContractPriceRank.vue

@@ -0,0 +1,107 @@
+<!-- 合同金额排行 -->
+<template>
+  <!-- 柱状图 -->
+  <el-card shadow="never">
+    <el-skeleton :loading="loading" animated>
+      <Echart :height="500" :options="echartsOption" />
+    </el-skeleton>
+  </el-card>
+
+  <!-- 排行列表 -->
+  <el-card shadow="never" class="mt-16px">
+    <el-table v-loading="loading" :data="list">
+      <el-table-column label="公司排名" align="center" type="index" width="80" />
+      <el-table-column label="签订人" align="center" prop="nickname" min-width="200" />
+      <el-table-column label="部门" align="center" prop="deptName" min-width="200" />
+      <el-table-column
+        label="合同金额(元)"
+        align="center"
+        prop="count"
+        min-width="200"
+        :formatter="fenToYuanFormat"
+      />
+    </el-table>
+  </el-card>
+</template>
+<script setup lang="ts">
+import { RankApi, BiRankRespVO } from '@/api/crm/bi/rank'
+import { EChartsOption } from 'echarts'
+import { fenToYuanFormat } from '@/utils/formatter'
+import { fenToYuan } from '@/utils'
+import { clone } from 'unocss'
+
+defineOptions({ name: 'ContractPriceRank' })
+const props = defineProps<{ queryParams: any }>() // 搜索参数
+
+const loading = ref(false) // 加载中
+const list = ref<BiRankRespVO[]>([]) // 列表的数据
+
+/** 柱状图配置:横向 */
+const echartsOption = reactive<EChartsOption>({
+  dataset: {
+    dimensions: ['nickname', 'count'],
+    source: []
+  },
+  grid: {
+    left: 20,
+    right: 20,
+    bottom: 20,
+    containLabel: true
+  },
+  legend: {
+    top: 50
+  },
+  series: [
+    {
+      name: '合同金额排行',
+      type: 'bar'
+    }
+  ],
+  toolbox: {
+    feature: {
+      dataZoom: {
+        yAxisIndex: false // 数据区域缩放:Y 轴不缩放
+      },
+      brush: {
+        type: ['lineX', 'clear'] // 区域缩放按钮、还原按钮
+      },
+      saveAsImage: { show: true, name: '合同金额排行' } // 保存为图片
+    }
+  },
+  tooltip: {
+    trigger: 'axis',
+    axisPointer: {
+      type: 'shadow'
+    },
+    valueFormatter: fenToYuan
+  },
+  xAxis: {
+    type: 'value',
+    name: '合同金额(元)'
+  },
+  yAxis: {
+    type: 'category',
+    name: '签订人'
+  }
+}) as EChartsOption
+
+/** 获取合同金额排行 */
+const loadData = async () => {
+  // 1. 加载排行数据
+  loading.value = true
+  const rankingList = await RankApi.getContractPriceRank(props.queryParams)
+  // 2.1 更新 Echarts 数据
+  if (echartsOption.dataset && echartsOption.dataset['source']) {
+    echartsOption.dataset['source'] = clone(rankingList).reverse()
+  }
+  // 2.2 更新列表数据
+  list.value = rankingList
+  loading.value = false
+}
+defineExpose({ loadData })
+
+/** 初始化 */
+onMounted(() => {
+  loadData()
+})
+</script>

+ 108 - 0
src/views/crm/bi/rank/ReceivablePriceRank.vue

@@ -0,0 +1,108 @@
+<!-- 回款金额排行 -->
+<template>
+  <!-- 柱状图 -->
+  <el-card shadow="never">
+    <el-skeleton :loading="loading" animated>
+      <Echart :height="500" :options="echartsOption" />
+    </el-skeleton>
+  </el-card>
+
+  <!-- 排行列表 -->
+  <el-card shadow="never" class="mt-16px">
+    <el-table v-loading="loading" :data="list">
+      <el-table-column label="公司排名" align="center" type="index" width="80" />
+      <el-table-column label="签订人" align="center" prop="nickname" min-width="200" />
+      <el-table-column label="部门" align="center" prop="deptName" min-width="200" />
+      <el-table-column
+        label="回款金额(元)"
+        align="center"
+        prop="count"
+        min-width="200"
+        :formatter="fenToYuanFormat"
+      />
+    </el-table>
+  </el-card>
+</template>
+<script setup lang="ts">
+import { RankApi, BiRankRespVO } from '@/api/crm/bi/rank'
+import { EChartsOption } from 'echarts'
+import { fenToYuanFormat } from '@/utils/formatter'
+import { fenToYuan } from '@/utils'
+import { clone } from 'unocss'
+
+defineOptions({ name: 'ReceivablePriceRank' })
+const props = defineProps<{ queryParams: any }>() // 搜索参数
+
+const loading = ref(false) // 加载中
+const list = ref<BiRankRespVO[]>([]) // 列表的数据
+
+/** 柱状图配置:横向 */
+const echartsOption = reactive<EChartsOption>({
+  dataset: {
+    dimensions: ['nickname', 'count'],
+    source: []
+  },
+  grid: {
+    left: 20,
+    right: 20,
+    bottom: 20,
+    containLabel: true
+  },
+  legend: {
+    top: 50
+  },
+  series: [
+    {
+      name: '回款金额排行',
+      type: 'bar'
+    }
+  ],
+  toolbox: {
+    feature: {
+      dataZoom: {
+        yAxisIndex: false // 数据区域缩放:Y 轴不缩放
+      },
+      brush: {
+        type: ['lineX', 'clear'] // 区域缩放按钮、还原按钮
+      },
+      saveAsImage: { show: true, name: '回款金额排行' } // 保存为图片
+    }
+  },
+  tooltip: {
+    trigger: 'axis',
+    axisPointer: {
+      type: 'shadow'
+    },
+    valueFormatter: fenToYuan
+  },
+  xAxis: {
+    type: 'value',
+    name: '回款金额(元)'
+  },
+  yAxis: {
+    type: 'category',
+    name: '签订人',
+    nameGap: 30
+  }
+}) as EChartsOption
+
+/** 获取回款金额排行 */
+const loadData = async () => {
+  // 1. 加载排行数据
+  loading.value = true
+  const rankingList = await RankApi.getReceivablePriceRank(props.queryParams)
+  // 2.1 更新 Echarts 数据
+  if (echartsOption.dataset && echartsOption.dataset['source']) {
+    echartsOption.dataset['source'] = clone(rankingList).reverse()
+  }
+  // 2.2 更新列表数据
+  list.value = rankingList
+  loading.value = false
+}
+defineExpose({ loadData })
+
+/** 初始化 */
+onMounted(() => {
+  loadData()
+})
+</script>

+ 100 - 0
src/views/crm/bi/rank/index.vue

@@ -0,0 +1,100 @@
+<!-- BI 排行版 -->
+<template>
+  <ContentWrap>
+    <!-- 搜索工作栏 -->
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="68px"
+    >
+      <el-form-item label="时间范围" prop="orderDate">
+        <el-date-picker
+          v-model="queryParams.times"
+          :shortcuts="defaultShortcuts"
+          class="!w-240px"
+          end-placeholder="结束日期"
+          start-placeholder="开始日期"
+          type="daterange"
+          value-format="YYYY-MM-DD HH:mm:ss"
+          :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+        />
+      </el-form-item>
+      <el-form-item label="归属部门" prop="deptId">
+        <el-tree-select
+          v-model="queryParams.deptId"
+          :data="deptList"
+          :props="defaultProps"
+          check-strictly
+          node-key="id"
+          placeholder="请选择归属部门"
+        />
+      </el-form-item>
+      <el-form-item>
+        <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
+        <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <!-- 排行数据 -->
+  <el-col>
+    <el-tabs v-model="activeTab">
+      <!-- 合同金额排行 -->
+      <el-tab-pane label="合同金额排行" name="contractPriceRank" lazy>
+        <ContractPriceRank :query-params="queryParams" ref="contractPriceRankRef" />
+      </el-tab-pane>
+      <!-- 回款金额排行 -->
+      <el-tab-pane label="回款金额排行" name="receivablePriceRank" lazy>
+        <ReceivablePriceRank :query-params="queryParams" ref="receivablePriceRankRef" />
+      </el-tab-pane>
+    </el-tabs>
+  </el-col>
+</template>
+<script lang="ts" setup>
+import ContractPriceRank from './ContractPriceRank.vue'
+import ReceivablePriceRank from './ReceivablePriceRank.vue'
+import { defaultProps, handleTree } from '@/utils/tree'
+import * as DeptApi from '@/api/system/dept'
+import { beginOfDay, defaultShortcuts, endOfDay, formatDate } from '@/utils/formatTime'
+import { useUserStore } from '@/store/modules/user'
+
+defineOptions({ name: 'CrmBiRank' })
+
+const queryParams = reactive({
+  deptId: useUserStore().getUser.deptId,
+  times: [
+    // 默认显示最近一周的数据
+    formatDate(beginOfDay(new Date(new Date().getTime() - 3600 * 1000 * 24 * 7))),
+    formatDate(endOfDay(new Date(new Date().getTime() - 3600 * 1000 * 24)))
+  ]
+})
+
+const queryFormRef = ref() // 搜索的表单
+const deptList = ref<Tree[]>([]) // 树形结构
+const activeTab = ref('contractPriceRank')
+const contractPriceRankRef = ref() // ContractPriceRank 组件的引用
+const receivablePriceRankRef = ref() // ReceivablePriceRank 组件的引用
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  if (activeTab.value === 'contractPriceRank') {
+    contractPriceRankRef.value.loadData()
+  } else if (activeTab.value === 'receivablePriceRank') {
+    receivablePriceRankRef.value.loadData()
+  }
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value.resetFields()
+  handleQuery()
+}
+
+// 加载部门树
+onMounted(async () => {
+  deptList.value = handleTree(await DeptApi.getSimpleDeptList())
+})
+</script>
+<style lang="scss" scoped></style>

+ 0 - 133
src/views/crm/bi/ranking/components/RankingContractStatistics.vue

@@ -1,133 +0,0 @@
-<template>
-  <el-card shadow="never">
-    <!-- 柱状图 -->
-    <el-skeleton :loading="trendLoading" animated>
-      <Echart :height="500" :options="barChartOptions" />
-    </el-skeleton>
-  </el-card>
-  <el-card shadow="never" class="mt-16px">
-    <!-- 排行列表 -->
-    <el-table v-loading="loading" :data="list">
-      <el-table-column label="公司排名" align="center" type="index" width="80" />
-      <el-table-column label="签订人" align="center" prop="nickname" min-width="200" />
-      <el-table-column label="部门" align="center" prop="deptName" min-width="200" />
-      <el-table-column label="合同金额(元)" align="center" prop="price" min-width="200" />
-    </el-table>
-  </el-card>
-</template>
-<script setup lang="ts">
-import { RankingStatisticsApi, BiContractRanKingRespVO, BiRankReqVO } from '@/api/crm/bi/ranking'
-import { EChartsOption } from 'echarts'
-
-/** 合同金额排行 */
-defineOptions({ name: 'RankingContractStatistics' })
-
-const trendLoading = ref(true) // 状态加载中
-const loading = ref(false) // 列表的加载中
-const list = ref<BiContractRanKingRespVO[]>([]) // 列表的数据
-const params = defineProps<{ queryParams: BiRankReqVO }>() // 搜索参数
-
-/** 柱状图配置 横向 */
-const barChartOptions = reactive<EChartsOption>({
-  dataset: {
-    dimensions: ['name', 'value'],
-    source: []
-  },
-  grid: {
-    left: 20,
-    right: 20,
-    bottom: 20,
-    top: 80,
-    containLabel: true
-  },
-  legend: {
-    top: 50
-  },
-  series: [
-    {
-      name: '合同金额排行',
-      type: 'bar',
-      smooth: true,
-      itemStyle: { color: '#B37FEB' }
-    }
-  ],
-  toolbox: {
-    feature: {
-      // 数据区域缩放
-      dataZoom: {
-        yAxisIndex: false // Y轴不缩放
-      },
-      brush: {
-        type: ['lineX', 'clear'] // 区域缩放按钮、还原按钮
-      },
-      saveAsImage: { show: true, name: '合同金额排行' } // 保存为图片
-    }
-  },
-  tooltip: {
-    trigger: 'axis',
-    axisPointer: {
-      type: 'shadow'
-    }
-  },
-  xAxis: {
-    type: 'value',
-    name: '合同金额(元)',
-    nameGap: 30,
-    nameTextStyle: {
-      color: '#666',
-      fontSize: 14
-    }
-  },
-  yAxis: {
-    type: 'category',
-    name: '签订人',
-    nameGap: 30,
-    nameTextStyle: {
-      color: '#666',
-      fontSize: 14
-    },
-    axisLabel: {
-      formatter: (value: string) => {
-        return value
-      }
-    }
-  }
-}) as EChartsOption
-
-/** 获取合同金额排行 */
-const getRankingContractStatistics = async () => {
-  trendLoading.value = true
-  loading.value = true
-  const rankingList = await RankingStatisticsApi.contractAmountRanking(params.queryParams)
-  let source = rankingList.map((item: BiContractRanKingRespVO) => {
-    return {
-      name: item.nickname,
-      value: item.price
-    }
-  })
-  // 反转数据源
-  source = source.reverse()
-  // 更新 Echarts 数据
-  if (barChartOptions.dataset && barChartOptions.dataset['source']) {
-    barChartOptions.dataset['source'] = source
-  }
-  // 更新列表数据
-  list.value = rankingList
-  trendLoading.value = false
-  loading.value = false
-}
-
-/** 重新加载数据 */
-const reloadData = async () => {
-  await getRankingContractStatistics()
-}
-// 暴露 reloadData 函数
-defineExpose({
-  reloadData
-})
-
-onMounted(() => {
-  getRankingContractStatistics()
-})
-</script>
-<style scoped lang="scss"></style>

+ 0 - 133
src/views/crm/bi/ranking/components/RankingReceivablesStatistics.vue

@@ -1,133 +0,0 @@
-<template>
-  <el-card shadow="never">
-    <!-- 柱状图 -->
-    <el-skeleton :loading="trendLoading" animated>
-      <Echart :height="500" :options="barChartOptions" />
-    </el-skeleton>
-  </el-card>
-  <el-card shadow="never" class="mt-16px">
-    <!-- 排行列表 -->
-    <el-table v-loading="loading" :data="list">
-      <el-table-column label="公司排名" align="center" type="index" width="80" />
-      <el-table-column label="签订人" align="center" prop="nickname" min-width="200" />
-      <el-table-column label="部门" align="center" prop="deptName" min-width="200" />
-      <el-table-column label="合同金额(元)" align="center" prop="price" min-width="200" />
-    </el-table>
-  </el-card>
-</template>
-<script setup lang="ts">
-import { RankingStatisticsApi, BiReceivablesRanKingRespVO } from '@/api/crm/bi/ranking'
-import { EChartsOption } from 'echarts'
-
-/** 回款金额排行 */
-defineOptions({ name: 'RankingReceivablesStatistics' })
-
-const trendLoading = ref(true) // 状态加载中
-const loading = ref(false) // 列表的加载中
-const list = ref<BiReceivablesRanKingRespVO[]>([]) // 列表的数据
-const params = defineProps<{ queryParams: any }>() // 搜索参数
-
-/** 柱状图配置 横向 */
-const barChartOptions = reactive<EChartsOption>({
-  dataset: {
-    dimensions: ['name', 'value'],
-    source: []
-  },
-  grid: {
-    left: 20,
-    right: 20,
-    bottom: 20,
-    top: 80,
-    containLabel: true
-  },
-  legend: {
-    top: 50
-  },
-  series: [
-    {
-      name: '回款金额排行',
-      type: 'bar',
-      smooth: true,
-      itemStyle: { color: '#B37FEB' }
-    }
-  ],
-  toolbox: {
-    feature: {
-      // 数据区域缩放
-      dataZoom: {
-        yAxisIndex: false // Y轴不缩放
-      },
-      brush: {
-        type: ['lineX', 'clear'] // 区域缩放按钮、还原按钮
-      },
-      saveAsImage: { show: true, name: '回款金额排行' } // 保存为图片
-    }
-  },
-  tooltip: {
-    trigger: 'axis',
-    axisPointer: {
-      type: 'shadow'
-    }
-  },
-  xAxis: {
-    type: 'value',
-    name: '回款金额(元)',
-    nameGap: 30,
-    nameTextStyle: {
-      color: '#666',
-      fontSize: 14
-    }
-  },
-  yAxis: {
-    type: 'category',
-    name: '签订人',
-    nameGap: 30,
-    nameTextStyle: {
-      color: '#666',
-      fontSize: 14
-    },
-    axisLabel: {
-      formatter: (value: string) => {
-        return value
-      }
-    }
-  }
-}) as EChartsOption
-
-/** 获取回款金额排行 */
-const getRankingReceivablesStatistics = async () => {
-  trendLoading.value = true
-  loading.value = true
-  const rankingList = await RankingStatisticsApi.receivablesAmountRanking(params.queryParams)
-  let source = rankingList.map((item: BiReceivablesRanKingRespVO) => {
-    return {
-      name: item.nickname,
-      value: item.price
-    }
-  })
-  // 反转数据源
-  source = source.reverse()
-  // 更新 Echarts 数据
-  if (barChartOptions.dataset && barChartOptions.dataset['source']) {
-    barChartOptions.dataset['source'] = source
-  }
-  // 更新列表数据
-  list.value = rankingList
-  trendLoading.value = false
-  loading.value = false
-}
-
-/** 重新加载数据 */
-const reloadData = async () => {
-  await getRankingReceivablesStatistics()
-}
-// 暴露 reloadData 函数
-defineExpose({
-  reloadData
-})
-
-onMounted(() => {
-  getRankingReceivablesStatistics()
-})
-</script>
-<style scoped lang="scss"></style>

+ 0 - 91
src/views/crm/bi/ranking/index.vue

@@ -1,91 +0,0 @@
-<template>
-  <ContentWrap>
-    <!-- 搜索工作栏 -->
-    <el-form
-      class="-mb-15px"
-      :model="queryParams"
-      ref="queryFormRef"
-      :inline="true"
-      label-width="68px"
-    >
-      <el-form-item label="类型" prop="type">
-        <el-select v-model="queryParams.type" placeholder="请选择类型" clearable class="!w-240px">
-          <el-option
-            v-for="dict in getIntDictOptions(DICT_TYPE.BI_ANALYZE_TYPE)"
-            :key="dict.value"
-            :label="dict.label"
-            :value="dict.value"
-          />
-        </el-select>
-      </el-form-item>
-      <el-form-item label="归属部门" prop="deptId">
-        <el-tree-select
-          v-model="queryParams.deptId"
-          :data="deptList"
-          :props="defaultProps"
-          check-strictly
-          node-key="id"
-          placeholder="请选择归属部门"
-        />
-      </el-form-item>
-      <el-form-item>
-        <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
-        <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
-      </el-form-item>
-    </el-form>
-  </ContentWrap>
-  <el-col>
-    <el-tabs v-model="activeTab">
-      <el-tab-pane label="合同金额排行" name="contractAmountRanking">
-        <!-- 合同金额排行 -->
-        <RankingContractStatistics :queryParams="queryParams" ref="rankingContractStatisticsRef" />
-      </el-tab-pane>
-      <el-tab-pane label="回款金额排行" name="receivablesRanKing" lazy>
-        <!-- 回款金额排行 -->
-        <RankingReceivablesStatistics
-          :queryParams="queryParams"
-          ref="rankingReceivablesStatisticsRef"
-        />
-      </el-tab-pane>
-    </el-tabs>
-  </el-col>
-</template>
-<script lang="ts" setup>
-import RankingContractStatistics from './components/RankingContractStatistics.vue'
-import { defaultProps, handleTree } from '@/utils/tree'
-import * as DeptApi from '@/api/system/dept'
-import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
-
-/** 排行榜 */
-defineOptions({ name: 'RankingStatistics' })
-
-const queryParams = reactive({
-  type: 9, // 将 type 的初始值设置为 9 本年
-  deptId: null
-})
-const queryFormRef = ref() // 搜索的表单
-const deptList = ref<Tree[]>([]) // 树形结构
-const activeTab = ref('contractAmountRanking')
-const rankingContractStatisticsRef = ref() // RankingContractStatistics组件的引用
-const rankingReceivablesStatisticsRef = ref() // RankingReceivablesStatistics组件的引用
-
-/** 搜索按钮操作 */
-const handleQuery = () => {
-  if (activeTab.value === 'contractAmountRanking') {
-    rankingContractStatisticsRef.value.reloadData()
-  } else if (activeTab.value === 'receivablesRanKing') {
-    rankingReceivablesStatisticsRef.value.reloadData()
-  }
-}
-
-/** 重置按钮操作 */
-const resetQuery = () => {
-  queryFormRef.value.resetFields()
-  handleQuery()
-}
-// 加载部门树
-onMounted(async () => {
-  deptList.value = handleTree(await DeptApi.getSimpleDeptList())
-})
-</script>
-<style lang="scss" scoped></style>

+ 8 - 6
src/views/crm/contact/detail/index.vue

@@ -57,6 +57,7 @@ const message = useMessage()
 const id = Number(route.params.id) // 联系人编号
 const loading = ref(true) // 加载中
 const contact = ref<ContactApi.ContactVO>({} as ContactApi.ContactVO) // 联系人详情
+const permissionListRef = ref<InstanceType<typeof PermissionList>>() // 团队成员列表 Ref
 
 /** 获取详情 */
 const getContactData = async (id: number) => {
@@ -68,22 +69,20 @@ const getContactData = async (id: number) => {
     loading.value = false
   }
 }
+
 /** 编辑 */
 const formRef = ref()
 const openForm = (type: string, id?: number) => {
   formRef.value.open(type, id)
 }
+
 /** 联系人转移 */
 const crmTransferFormRef = ref<InstanceType<typeof CrmTransferForm>>() // 联系人转移表单 ref
 const transfer = () => {
-  crmTransferFormRef.value?.open('联系人转移', contact.value.id, ContactApi.transfer)
+  crmTransferFormRef.value?.open('联系人转移', contact.value.id, ContactApi.transferContact)
 }
 
-const permissionListRef = ref<InstanceType<typeof PermissionList>>() // 团队成员列表 Ref
-
-/**
- * 获取操作日志
- */
+/** 获取操作日志 */
 const logList = ref<OperateLogV2VO[]>([]) // 操作日志列表
 const getOperateLog = async (contactId: number) => {
   if (!contactId) {
@@ -95,9 +94,12 @@ const getOperateLog = async (contactId: number) => {
   })
   logList.value = data.list
 }
+
+/** 关闭窗口 */
 const close = () => {
   delView(unref(currentRoute))
 }
+
 /** 初始化 */
 const { delView } = useTagsViewStore() // 视图操作
 const { currentRoute } = useRouter() // 路由

+ 1 - 1
src/views/crm/contact/index.vue

@@ -22,7 +22,7 @@
             v-for="item in customerList"
             :key="item.id"
             :label="item.name"
-            :value="item.id"
+            :value="item.id!"
           />
         </el-select>
       </el-form-item>

+ 83 - 83
src/views/crm/contract/ContractForm.vue

@@ -1,5 +1,5 @@
 <template>
-  <Dialog v-model="dialogVisible" :title="dialogTitle" width="70%">
+  <Dialog v-model="dialogVisible" :title="dialogTitle" width="820">
     <el-form
       ref="formRef"
       v-loading="formLoading"
@@ -7,37 +7,22 @@
       :rules="formRules"
       label-width="110px"
     >
-      <el-row>
-        <el-col :span="24" class="mb-10px">
-          <CardTitle title="基本信息" />
-        </el-col>
-        <el-col :span="12">
-          <el-form-item label="合同名称" prop="name">
-            <el-input v-model="formData.name" placeholder="请输入合同名称" />
-          </el-form-item>
-        </el-col>
+      <el-row :gutter="20">
         <el-col :span="12">
           <el-form-item label="合同编号" prop="no">
             <el-input v-model="formData.no" placeholder="请输入合同编号" />
           </el-form-item>
         </el-col>
         <el-col :span="12">
-          <el-form-item label="客户" prop="customerId">
-            <el-select v-model="formData.customerId">
-              <el-option
-                v-for="item in customerList"
-                :key="item.id"
-                :label="item.name"
-                :value="item.id!"
-              />
-            </el-select>
+          <el-form-item label="合同名称" prop="name">
+            <el-input v-model="formData.name" placeholder="请输入合同名称" />
           </el-form-item>
         </el-col>
         <el-col :span="12">
-          <el-form-item label="客户签约人" prop="contactId">
-            <el-select v-model="formData.contactId" :disabled="!formData.customerId">
+          <el-form-item label="客户名称" prop="customerId">
+            <el-select v-model="formData.customerId">
               <el-option
-                v-for="item in getContactOptions"
+                v-for="item in customerList"
                 :key="item.id"
                 :label="item.name"
                 :value="item.id!"
@@ -45,30 +30,6 @@
             </el-select>
           </el-form-item>
         </el-col>
-        <el-col :span="12">
-          <el-form-item label="公司签约人" prop="signUserId">
-            <el-select v-model="formData.signUserId">
-              <el-option
-                v-for="item in userList"
-                :key="item.id"
-                :label="item.nickname"
-                :value="item.id!"
-              />
-            </el-select>
-          </el-form-item>
-        </el-col>
-        <el-col :span="12">
-          <el-form-item label="负责人" prop="ownerUserId">
-            <el-select v-model="formData.ownerUserId">
-              <el-option
-                v-for="item in userList"
-                :key="item.id"
-                :label="item.nickname"
-                :value="item.id!"
-              />
-            </el-select>
-          </el-form-item>
-        </el-col>
         <el-col :span="12">
           <el-form-item label="商机名称" prop="businessId">
             <el-select v-model="formData.businessId">
@@ -81,11 +42,6 @@
             </el-select>
           </el-form-item>
         </el-col>
-        <el-col :span="12">
-          <el-form-item label="合同金额(元)" prop="price">
-            <el-input v-model="formData.price" placeholder="请输入合同金额" />
-          </el-form-item>
-        </el-col>
         <el-col :span="12">
           <el-form-item label="下单日期" prop="orderDate">
             <el-date-picker
@@ -96,6 +52,11 @@
             />
           </el-form-item>
         </el-col>
+        <el-col :span="12">
+          <el-form-item label="合同金额" prop="price">
+            <el-input v-model="formData.price" placeholder="请输入合同金额" />
+          </el-form-item>
+        </el-col>
         <el-col :span="12">
           <el-form-item label="开始时间" prop="startTime">
             <el-date-picker
@@ -116,6 +77,42 @@
             />
           </el-form-item>
         </el-col>
+        <el-col :span="12">
+          <el-form-item label="公司签约人" prop="signUserId">
+            <el-select v-model="formData.signUserId">
+              <el-option
+                v-for="item in userList"
+                :key="item.id"
+                :label="item.nickname"
+                :value="item.id!"
+              />
+            </el-select>
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="客户签约人" prop="contactId">
+            <el-select v-model="formData.contactId" :disabled="!formData.customerId">
+              <el-option
+                v-for="item in getContactOptions"
+                :key="item.id"
+                :label="item.name"
+                :value="item.id!"
+              />
+            </el-select>
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="负责人" prop="ownerUserId">
+            <el-select v-model="formData.ownerUserId">
+              <el-option
+                v-for="item in userList"
+                :key="item.id"
+                :label="item.nickname"
+                :value="item.id!"
+              />
+            </el-select>
+          </el-form-item>
+        </el-col>
         <el-col :span="24">
           <el-form-item label="备注" prop="remark">
             <el-input
@@ -126,6 +123,7 @@
             />
           </el-form-item>
         </el-col>
+        <!-- TODO @puhui999:productItems 改成 products 会更好点;因为它不是 item 哈 -->
         <el-col :span="24">
           <el-form-item label="产品列表" prop="productList">
             <ProductList v-model="formData.productItems" />
@@ -133,27 +131,21 @@
         </el-col>
         <el-col :span="12">
           <el-form-item label="整单折扣(%)" prop="discountPercent">
-            <el-input v-model="formData.discountPercent" placeholder="请输入整单折扣" />
+            <el-input-number
+              v-model="formData.discountPercent"
+              :min="0"
+              :max="100"
+              :precision="0"
+              placeholder="请输入整单折扣"
+              class="!w-100%"
+            />
           </el-form-item>
         </el-col>
         <el-col :span="12">
           <el-form-item label="产品总金额(元)" prop="productPrice">
-            <el-input v-model="formData.productPrice" placeholder="请输入产品总金额" />
+            {{ fenToYuan(formData.productPrice) }}
           </el-form-item>
         </el-col>
-        <el-col :span="24">
-          <CardTitle class="mb-10px" title="审批信息" />
-        </el-col>
-        <el-col :span="12">
-          <el-button
-            class="m-20px"
-            link
-            type="primary"
-            @click="BPMLModelRef?.handleBpmnDetail('contract-approve')"
-          >
-            查看工作流
-          </el-button>
-        </el-col>
       </el-row>
     </el-form>
     <template #footer>
@@ -161,7 +153,6 @@
       <el-button @click="dialogVisible = false">取 消</el-button>
     </template>
   </Dialog>
-  <BPMLModel ref="BPMLModelRef" />
 </template>
 <script lang="ts" setup>
 import * as CustomerApi from '@/api/crm/customer'
@@ -170,7 +161,7 @@ import * as UserApi from '@/api/system/user'
 import * as ContactApi from '@/api/crm/contact'
 import * as BusinessApi from '@/api/crm/business'
 import ProductList from './components/ProductList.vue'
-import BPMLModel from '@/views/crm/contract/components/BPMLModel.vue'
+import { fenToYuan } from '@/utils'
 
 const { t } = useI18n() // 国际化
 const message = useMessage() // 消息弹窗
@@ -188,7 +179,8 @@ const formRules = reactive({
   no: [{ required: true, message: '合同编号不能为空', trigger: 'blur' }]
 })
 const formRef = ref() // 表单 Ref
-const BPMLModelRef = ref<InstanceType<typeof BPMLModel>>()
+
+/** 监听合同产品变化,计算合同产品总价 */
 watch(
   () => formData.value.productItems,
   (val) => {
@@ -196,7 +188,7 @@ watch(
       formData.value.productPrice = 0
       return
     }
-    // 使用reduce函数进行累加
+    // 使用 reduce 函数进行累加
     formData.value.productPrice = val.reduce(
       (accumulator, currentValue) =>
         isNaN(accumulator + currentValue.totalPrice) ? 0 : accumulator + currentValue.totalPrice,
@@ -205,13 +197,13 @@ watch(
   },
   { deep: true }
 )
+
 /** 打开弹窗 */
 const open = async (type: string, id?: number) => {
   dialogVisible.value = true
   dialogTitle.value = t('action.' + type)
   formType.value = type
   resetForm()
-  await getAllApi()
   // 修改时,设置数据
   if (id) {
     formLoading.value = true
@@ -221,9 +213,7 @@ const open = async (type: string, id?: number) => {
       formLoading.value = false
     }
   }
-}
-const getAllApi = async () => {
-  await Promise.all([getCustomerList(), getUserList(), getContactListList(), getBusinessList()])
+  await getAllApi()
 }
 defineExpose({ open }) // 提供 open 方法,用于打开弹窗
 
@@ -252,32 +242,42 @@ const submitForm = async () => {
     formLoading.value = false
   }
 }
-const customerList = ref<CustomerApi.CustomerVO[]>([])
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {} as ContractApi.ContractVO
+  formRef.value?.resetFields()
+}
+
+/** 获取其它相关数据 */
+const getAllApi = async () => {
+  await Promise.all([getCustomerList(), getUserList(), getContactListList(), getBusinessList()])
+}
+
 /** 获取客户 */
+const customerList = ref<CustomerApi.CustomerVO[]>([])
 const getCustomerList = async () => {
   customerList.value = await CustomerApi.getSimpleCustomerList()
 }
-const contactList = ref<ContactApi.ContactVO[]>([])
+
 /** 动态获取客户联系人 */
+const contactList = ref<ContactApi.ContactVO[]>([])
 const getContactOptions = computed(() =>
   contactList.value.filter((item) => item.customerId === formData.value.customerId)
 )
 const getContactListList = async () => {
   contactList.value = await ContactApi.getSimpleContactList()
 }
-const userList = ref<UserApi.UserVO[]>([])
+
 /** 获取用户列表 */
+const userList = ref<UserApi.UserVO[]>([])
 const getUserList = async () => {
   userList.value = await UserApi.getSimpleUserList()
 }
-const businessList = ref<BusinessApi.BusinessVO[]>([])
+
 /** 获取商机 */
+const businessList = ref<BusinessApi.BusinessVO[]>([])
 const getBusinessList = async () => {
   businessList.value = await BusinessApi.getSimpleBusinessList()
 }
-/** 重置表单 */
-const resetForm = () => {
-  formData.value = {} as ContractApi.ContractVO
-  formRef.value?.resetFields()
-}
 </script>

+ 0 - 31
src/views/crm/contract/components/BPMLModel.vue

@@ -1,31 +0,0 @@
-<template>
-  <!-- 弹窗:流程模型图的预览 -->
-  <Dialog v-model="bpmnDetailVisible" :append-to-body="true" title="流程图" width="800">
-    <MyProcessViewer
-      key="designer"
-      v-model="bpmnXML"
-      :prefix="bpmnControlForm.prefix"
-      :value="bpmnXML as any"
-      v-bind="bpmnControlForm"
-    />
-  </Dialog>
-</template>
-
-<script lang="ts" setup>
-import * as ModelApi from '@/api/bpm/model'
-import { MyProcessViewer } from '@/components/bpmnProcessDesigner/package'
-
-defineOptions({ name: 'BPMLModel' })
-/** 流程图的详情按钮操作 */
-const bpmnDetailVisible = ref(false)
-const bpmnXML = ref(null)
-const bpmnControlForm = ref({
-  prefix: 'flowable'
-})
-const handleBpmnDetail = async (key: string) => {
-  const data = await ModelApi.getModelByKey(key)
-  bpmnXML.value = data.bpmnXml || ''
-  bpmnDetailVisible.value = true
-}
-defineExpose({ handleBpmnDetail })
-</script>

+ 83 - 24
src/views/crm/contract/components/ProductList.vue

@@ -1,39 +1,59 @@
+<!-- 合同 Form 表单下的 Product 列表 -->
 <template>
   <el-row justify="end">
     <el-button plain type="primary" @click="openForm">添加产品</el-button>
   </el-row>
   <el-table :data="list" :show-overflow-tooltip="true" :stripe="true">
-    <el-table-column align="center" label="产品名称" prop="name" width="160" />
-    <el-table-column align="center" label="产品类型" prop="categoryName" width="160" />
-    <el-table-column align="center" label="产品单位" prop="unit">
-      <template #default="scope">
-        <dict-tag :type="DICT_TYPE.CRM_PRODUCT_UNIT" :value="scope.row.unit" />
-      </template>
-    </el-table-column>
-    <el-table-column align="center" label="产品编码" prop="no" />
+    <el-table-column align="center" label="产品名称" prop="name" width="120" />
     <el-table-column
       :formatter="fenToYuanFormat"
       align="center"
-      label="价格(元)"
+      label="价格"
       prop="price"
       width="100"
     />
-    <el-table-column align="center" label="数量" prop="count" width="200">
+    <el-table-column align="center" label="产品类型" prop="categoryName" width="100" />
+    <el-table-column align="center" label="产品单位" prop="unit">
+      <template #default="scope">
+        <dict-tag :type="DICT_TYPE.CRM_PRODUCT_UNIT" :value="scope.row.unit" />
+      </template>
+    </el-table-column>
+    <el-table-column align="center" label="产品编码" prop="no" />
+    <el-table-column align="center" fixed="right" label="数量" prop="count" width="100">
       <template #default="{ row }: { row: ProductApi.ProductExpandVO }">
-        <el-input-number v-model="row.count" class="!w-100%" />
+        <el-input-number
+          v-model="row.count"
+          controls-position="right"
+          :min="0"
+          :precision="0"
+          class="!w-100%"
+        />
       </template>
     </el-table-column>
-    <el-table-column align="center" label="折扣(%)" prop="discountPercent" width="200">
+    <el-table-column
+      align="center"
+      fixed="right"
+      label="折扣(%)"
+      prop="discountPercent"
+      width="120"
+    >
       <template #default="{ row }: { row: ProductApi.ProductExpandVO }">
-        <el-input-number v-model="row.discountPercent" class="!w-100%" />
+        <el-input-number
+          v-model="row.discountPercent"
+          controls-position="right"
+          :min="0"
+          :max="100"
+          :precision="0"
+          class="!w-100%"
+        />
       </template>
     </el-table-column>
-    <el-table-column align="center" label="合计" prop="totalPrice" width="100">
+    <el-table-column align="center" fixed="right" label="合计" prop="totalPrice" width="100">
       <template #default="{ row }: { row: ProductApi.ProductExpandVO }">
-        {{ getTotalPrice(row) }}
+        {{ fenToYuan(getTotalPrice(row)) }}
       </template>
     </el-table-column>
-    <el-table-column align="center" fixed="right" label="操作" width="130">
+    <el-table-column align="center" fixed="right" label="操作" width="60">
       <template #default="scope">
         <el-button link type="danger" @click="handleDelete(scope.row.id)"> 移除</el-button>
       </template>
@@ -41,7 +61,7 @@
   </el-table>
 
   <!-- table 选择表单 -->
-  <TableSelectForm ref="tableSelectFormRef" v-model="multipleSelection" title="选择品">
+  <TableSelectForm ref="tableSelectFormRef" v-model="multipleSelection" title="选择品">
     <el-table-column align="center" label="产品名称" prop="name" width="160" />
     <el-table-column align="center" label="产品类型" prop="categoryName" width="160" />
     <el-table-column align="center" label="产品单位" prop="unit">
@@ -65,29 +85,61 @@ import * as ProductApi from '@/api/crm/product'
 import { DICT_TYPE } from '@/utils/dict'
 import { fenToYuanFormat } from '@/utils/formatter'
 import { TableSelectForm } from '@/components/Table/index'
+import { fenToYuan, floatToFixed2, yuanToFen } from '@/utils'
 
 defineOptions({ name: 'ProductList' })
-withDefaults(defineProps<{ modelValue: any[] }>(), { modelValue: () => [] })
+const props = withDefaults(defineProps<{ modelValue: ProductApi.ProductExpandVO[] }>(), {
+  modelValue: () => []
+})
 const emits = defineEmits<{
   (e: 'update:modelValue', v: any[]): void
 }>()
-const list = ref<ProductApi.ProductExpandVO[]>([])
+
+const list = ref<ProductApi.ProductExpandVO[]>([]) // 已添加的产品列表
+const multipleSelection = ref<ProductApi.ProductExpandVO[]>([]) // 多选
+
+/** 处理删除 */
 const handleDelete = (id: number) => {
   const index = list.value.findIndex((item) => item.id === id)
   if (index !== -1) {
     list.value.splice(index, 1)
   }
 }
+
+/** 打开 Product 弹窗  */
 const tableSelectFormRef = ref<InstanceType<typeof TableSelectForm>>()
-const multipleSelection = ref<ProductApi.ProductExpandVO[]>([])
 const openForm = () => {
   tableSelectFormRef.value?.open(ProductApi.getProductPage)
 }
+
+/** 计算 totalPrice */
 const getTotalPrice = computed(() => (row: ProductApi.ProductExpandVO) => {
-  const totalPrice = (row.price * row.count * row.discountPercent) / 100
-  row.totalPrice = isNaN(totalPrice) ? 0 : totalPrice
-  return isNaN(totalPrice) ? 0 : totalPrice
+  const totalPrice =
+    (Number(row.price) / 100) * Number(row.count) * (1 - Number(row.discountPercent) / 100)
+  row.totalPrice = isNaN(totalPrice) ? 0 : yuanToFen(totalPrice)
+  return isNaN(totalPrice) ? 0 : totalPrice.toFixed(2)
 })
+
+/** 编辑时合同产品回显 */
+const isSetListValue = ref(false) // 判断是否已经给 list 赋值过,用于编辑表单产品回显
+watch(
+  () => props.modelValue,
+  (val) => {
+    if (!val || val.length === 0 || isSetListValue.value) {
+      return
+    }
+    list.value = [
+      ...props.modelValue.map((item) => {
+        item.totalPrice = floatToFixed2(item.totalPrice) as unknown as number
+        return item
+      })
+    ]
+    isSetListValue.value = true
+  },
+  { immediate: true, deep: true }
+)
+
+/** 监听列表变化,动态更新合同产品列表 */
 watch(
   list,
   (val) => {
@@ -98,14 +150,21 @@ watch(
   },
   { deep: true }
 )
+
+// 监听产品选择结果动态添加产品到列表中,如果产品存在则不放入列表中
 watch(
   multipleSelection,
   (val) => {
     if (!val || val.length === 0) {
       return
     }
+    // 过滤出不在列表中的产品
     const ids = list.value.map((item) => item.id)
-    list.value.push(...multipleSelection.value.filter((item) => ids.indexOf(item.id) === -1))
+    const productList = multipleSelection.value.filter((item) => ids.indexOf(item.id) === -1)
+    if (!productList || productList.length === 0) {
+      return
+    }
+    list.value.push(...productList)
   },
   { deep: true }
 )

+ 12 - 7
src/views/crm/contract/detail/ContractDetailsHeader.vue

@@ -1,3 +1,4 @@
+<!-- 合同详情头部组件-->
 <template>
   <div>
     <div class="flex items-start justify-between">
@@ -16,17 +17,20 @@
   </div>
   <ContentWrap class="mt-10px">
     <el-descriptions :column="5" direction="vertical">
-      <el-descriptions-item label="客户">
+      <el-descriptions-item label="客户名称">
         {{ contract.customerName }}
       </el-descriptions-item>
-      <el-descriptions-item label="客户签约人">
-        {{ contract.contactName }}
+      <el-descriptions-item label="合同金额(元)">
+        {{ floatToFixed2(contract.price) }}
       </el-descriptions-item>
-      <el-descriptions-item label="合同金额">
-        {{ contract.productPrice }}
+      <el-descriptions-item label="下单时间">
+        {{ contract.orderDate ? formatDate(contract.orderDate) : '空' }}
       </el-descriptions-item>
-      <el-descriptions-item label="创建时间">
-        {{ contract.createTime ? formatDate(contract.createTime) : '空' }}
+      <el-descriptions-item label="回款金额(元)">
+        {{ floatToFixed2(contract.price) }}
+      </el-descriptions-item>
+      <el-descriptions-item label="负责人">
+        {{ contract.ownerUserName }}
       </el-descriptions-item>
     </el-descriptions>
   </ContentWrap>
@@ -34,6 +38,7 @@
 <script lang="ts" setup>
 import * as ContractApi from '@/api/crm/contract'
 import { formatDate } from '@/utils/formatTime'
+import { floatToFixed2 } from '@/utils'
 
 defineOptions({ name: 'ContractDetailsHeader' })
 defineProps<{ contract: ContractApi.ContractVO }>()

+ 32 - 2
src/views/crm/contract/detail/ContractDetailsInfo.vue

@@ -1,3 +1,4 @@
+<!-- 合同详情组件 -->
 <template>
   <ContentWrap>
     <el-collapse v-model="activeNames">
@@ -5,14 +6,43 @@
         <template #title>
           <span class="text-base font-bold">基本信息</span>
         </template>
-        <!-- TODO puhui999: 先出详情样式后补全 -->
-        <el-descriptions :column="4">
+        <el-descriptions :column="3">
+          <el-descriptions-item label="合同编号">
+            {{ contract.no }}
+          </el-descriptions-item>
           <el-descriptions-item label="合同名称">
             {{ contract.name }}
           </el-descriptions-item>
+          <el-descriptions-item label="客户名称">
+            {{ contract.customerName }}
+          </el-descriptions-item>
+          <el-descriptions-item label="商机名称">
+            {{ contract.businessName }}
+          </el-descriptions-item>
+          <el-descriptions-item label="合同金额(元)">
+            {{ contract.price }}
+          </el-descriptions-item>
+          <el-descriptions-item label="下单时间">
+            {{ formatDate(contract.orderDate) }}
+          </el-descriptions-item>
+          <el-descriptions-item label="开始时间">
+            {{ formatDate(contract.startTime) }}
+          </el-descriptions-item>
+          <el-descriptions-item label="结束时间">
+            {{ formatDate(contract.endTime) }}
+          </el-descriptions-item>
+          <el-descriptions-item label="客户签约人">
+            {{ contract.contactName }}
+          </el-descriptions-item>
+          <el-descriptions-item label="公司签约人">
+            {{ contract.signUserName }}
+          </el-descriptions-item>
           <el-descriptions-item label="备注">
             {{ contract.remark }}
           </el-descriptions-item>
+          <el-descriptions-item label="合同状态">
+            {{ contract.auditStatus }}
+          </el-descriptions-item>
         </el-descriptions>
       </el-collapse-item>
       <el-collapse-item name="systemInfo">

+ 67 - 0
src/views/crm/contract/detail/ContractProductList.vue

@@ -0,0 +1,67 @@
+<!-- 合同详情:产品列表 -->
+<template>
+  <el-table :data="list" :show-overflow-tooltip="true" :stripe="true">
+    <el-table-column align="center" label="产品名称" prop="name" width="160" />
+    <el-table-column align="center" label="产品类型" prop="categoryName" width="160" />
+    <el-table-column align="center" label="产品单位" prop="unit">
+      <template #default="scope">
+        <dict-tag :type="DICT_TYPE.CRM_PRODUCT_UNIT" :value="scope.row.unit" />
+      </template>
+    </el-table-column>
+    <el-table-column align="center" label="产品编码" prop="no" />
+    <el-table-column
+      :formatter="fenToYuanFormat"
+      align="center"
+      label="价格(元)"
+      prop="price"
+      width="100"
+    />
+    <el-table-column align="center" label="数量" prop="count" width="200" />
+    <el-table-column align="center" label="折扣(%)" prop="discountPercent" width="200" />
+    <el-table-column align="center" label="合计" prop="totalPrice" width="100">
+      <template #default="{ row }: { row: ProductApi.ProductExpandVO }">
+        {{ getTotalPrice(row) }}
+      </template>
+    </el-table-column>
+  </el-table>
+</template>
+
+<script lang="ts" setup>
+import { DICT_TYPE } from '@/utils/dict'
+import { fenToYuanFormat } from '@/utils/formatter'
+import * as ProductApi from '@/api/crm/product'
+import { floatToFixed2, yuanToFen } from '@/utils'
+
+defineOptions({ name: 'ContractProductList' })
+const props = withDefaults(defineProps<{ modelValue: ProductApi.ProductExpandVO[] }>(), {
+  modelValue: () => []
+})
+const list = ref<ProductApi.ProductExpandVO[]>([]) // 产品列表
+
+/** 计算 totalPrice */
+const getTotalPrice = computed(() => (row: ProductApi.ProductExpandVO) => {
+  const totalPrice =
+    (Number(row.price) / 100) * Number(row.count) * (1 - Number(row.discountPercent) / 100)
+  row.totalPrice = isNaN(totalPrice) ? 0 : yuanToFen(totalPrice)
+  return isNaN(totalPrice) ? 0 : totalPrice.toFixed(2)
+})
+
+/** 编辑时合同产品回显 */
+const isSetListValue = ref(false) // 判断是否已经给 list 赋值过,用于编辑表单产品回显
+watch(
+  () => props.modelValue,
+  (val) => {
+    if (!val || val.length === 0 || isSetListValue.value) {
+      return
+    }
+    list.value = [
+      ...props.modelValue.map((item) => {
+        item.totalPrice = floatToFixed2(item.totalPrice) as unknown as number
+        return item
+      })
+    ]
+    isSetListValue.value = true
+  },
+  { immediate: true, deep: true }
+)
+</script>

+ 25 - 17
src/views/crm/contract/detail/index.vue

@@ -1,20 +1,25 @@
+<!-- 合同详情页面组件-->
 <template>
   <ContractDetailsHeader v-loading="loading" :contract="contract">
     <el-button v-if="permissionListRef?.validateWrite" @click="openForm('update', contract.id)">
       编辑
     </el-button>
-    <el-button v-if="permissionListRef?.validateOwnerUser" type="primary" @click="transfer">
+    <el-button v-if="permissionListRef?.validateOwnerUser" type="primary" @click="transferContract">
       转移
     </el-button>
   </ContractDetailsHeader>
   <el-col>
     <el-tabs>
-      <el-tab-pane label="详细资料">
+      <!-- TODO @puhui999:跟进记录 -->
+      <el-tab-pane label="基本信息">
         <ContractDetailsInfo :contract="contract" />
       </el-tab-pane>
-      <el-tab-pane label="操作日志">
-        <OperateLogV2 :log-list="logList" />
+      <!-- TODO @puhui999:products 更合适哈 -->
+      <el-tab-pane label="产品">
+        <ContractProductList v-model="contract.productItems" />
       </el-tab-pane>
+      <!-- TODO @puhui999:回款信息 -->
+      <!-- TODO @puhui999:这里是不是不用 isPool 哈 -->
       <el-tab-pane label="团队成员">
         <PermissionList
           ref="permissionListRef"
@@ -24,18 +29,15 @@
           @quit-team="close"
         />
       </el-tab-pane>
-      <el-tab-pane label="商机" lazy>
-        <BusinessList
-          :biz-id="contract.id!"
-          :biz-type="BizTypeEnum.CRM_CONTRACT"
-          :customer-id="contract.customerId"
-        />
+      <el-tab-pane label="操作日志">
+        <OperateLogV2 :log-list="logList" />
       </el-tab-pane>
     </el-tabs>
   </el-col>
+
   <!-- 表单弹窗:添加/修改 -->
   <ContractForm ref="formRef" @success="getContractData" />
-  <CrmTransferForm ref="crmTransferFormRef" @success="close" />
+  <CrmTransferForm ref="transferFormRef" @success="close" />
 </template>
 <script lang="ts" setup>
 import { useTagsViewStore } from '@/store/modules/tagsView'
@@ -43,12 +45,12 @@ import { OperateLogV2VO } from '@/api/system/operatelog'
 import * as ContractApi from '@/api/crm/contract'
 import ContractDetailsHeader from './ContractDetailsHeader.vue'
 import ContractDetailsInfo from './ContractDetailsInfo.vue'
+import ContractProductList from './ContractProductList.vue'
 import { BizTypeEnum } from '@/api/crm/permission'
 import { getOperateLogPage } from '@/api/crm/operateLog'
 import ContractForm from '@/views/crm/contract/ContractForm.vue'
 import CrmTransferForm from '@/views/crm/permission/components/TransferForm.vue'
 import PermissionList from '@/views/crm/permission/components/PermissionList.vue'
-import BusinessList from '@/views/crm/business/components/BusinessList.vue'
 
 defineOptions({ name: 'CrmContractDetail' })
 
@@ -57,11 +59,14 @@ const message = useMessage()
 const contractId = ref(0) // 编号
 const loading = ref(true) // 加载中
 const contract = ref<ContractApi.ContractVO>({} as ContractApi.ContractVO) // 详情
+const permissionListRef = ref<InstanceType<typeof PermissionList>>() // 团队成员列表 Ref
+
 /** 编辑 */
 const formRef = ref()
 const openForm = (type: string, id?: number) => {
   formRef.value.open(type, id)
 }
+
 /** 获取详情 */
 const getContractData = async () => {
   loading.value = true
@@ -86,18 +91,21 @@ const getOperateLog = async (contractId: number) => {
   logList.value = data.list
 }
 
-const crmTransferFormRef = ref<InstanceType<typeof CrmTransferForm>>() // 合同转移表单 ref
-const transfer = () => {
-  crmTransferFormRef.value?.open('合同转移', contract.value.id, ContractApi.transfer)
+/** 转移 */
+// TODO @puhui999:这个组件,要不传递业务类型,然后组件里判断 title 和 api 能调用哪个;整体治理掉;
+const transferFormRef = ref<InstanceType<typeof CrmTransferForm>>() // 合同转移表单 ref
+const transferContract = () => {
+  transferFormRef.value?.open('合同转移', contract.value.id, ContractApi.transferContract)
 }
 
-const permissionListRef = ref<InstanceType<typeof PermissionList>>() // 团队成员列表 Ref
-/** 初始化 */
+/** 关闭 */
 const { delView } = useTagsViewStore() // 视图操作
 const { currentRoute } = useRouter() // 路由
 const close = () => {
   delView(unref(currentRoute))
 }
+
+/** 初始化 */
 onMounted(async () => {
   const id = route.params.id
   if (!id) {

+ 73 - 28
src/views/crm/contract/index.vue

@@ -54,47 +54,72 @@
   </ContentWrap>
 
   <!-- 列表 -->
-  <!-- TODO 芋艿:各种字段要调整 -->
   <ContentWrap>
     <el-table v-loading="loading" :data="list" :show-overflow-tooltip="true" :stripe="true">
-      <el-table-column align="center" label="合同编号" prop="id" />
-      <el-table-column align="center" label="合同名称" prop="name" />
-      <el-table-column align="center" label="客户名称" prop="customerId" />
-      <el-table-column align="center" label="商机名称" prop="businessId" />
-      <el-table-column align="center" label="工作流名称" prop="processInstanceId" />
+      <el-table-column align="center" fixed="left" label="合同编号" prop="no" width="130" />
+      <el-table-column align="center" label="合同名称" prop="name" width="130" />
+      <el-table-column align="center" label="客户名称" prop="customerName" width="120">
+        <template #default="scope">
+          <el-link
+            :underline="false"
+            type="primary"
+            @click="openCustomerDetail(scope.row.customerId)"
+          >
+            {{ scope.row.customerName }}
+          </el-link>
+        </template>
+      </el-table-column>
+      <!-- TODO @puhui999:做了商机详情后,可以把这个超链接加上 -->
+      <el-table-column align="center" label="商机名称" prop="businessName" width="130" />
       <el-table-column
-        :formatter="dateFormatter"
         align="center"
         label="下单时间"
         prop="orderDate"
-        width="180px"
+        width="120"
+        :formatter="dateFormatter2"
+      />
+      <el-table-column
+        align="center"
+        label="合同金额"
+        prop="price"
+        width="130"
+        :formatter="fenToYuanFormat"
       />
-      <el-table-column align="center" label="负责人" prop="ownerUserId" />
-      <el-table-column align="center" label="合同编号" prop="no" />
       <el-table-column
-        :formatter="dateFormatter"
         align="center"
-        label="开始时间"
+        label="合同开始时间"
         prop="startTime"
-        width="180px"
+        width="120"
+        :formatter="dateFormatter2"
       />
       <el-table-column
-        :formatter="dateFormatter"
         align="center"
-        label="结束时间"
+        label="合同结束时间"
         prop="endTime"
-        width="180px"
+        width="120"
+        :formatter="dateFormatter2"
       />
-      <el-table-column align="center" label="合同金额" prop="price" />
-      <el-table-column align="center" label="整单折扣" prop="discountPercent" />
-      <el-table-column align="center" label="产品总金额" prop="productPrice" />
-      <el-table-column align="center" label="联系人" prop="contactId" />
-      <el-table-column align="center" label="公司签约人" prop="signUserId" />
+      <el-table-column align="center" label="客户签约人" prop="contactName" width="130">
+        <template #default="scope">
+          <el-link
+            :underline="false"
+            type="primary"
+            @click="openContactDetail(scope.row.contactId)"
+          >
+            {{ scope.row.contactName }}
+          </el-link>
+        </template>
+      </el-table-column>
+      <el-table-column align="center" label="公司签约人" prop="signUserName" width="130" />
+      <el-table-column align="center" label="备注" prop="remark" width="130" />
+      <!-- TODO @puhui999:后续可加 【已收款金额】、【未收款金额】 -->
+      <el-table-column align="center" label="负责人" prop="ownerUserName" width="120" />
+      <el-table-column align="center" label="创建人" prop="creatorName" width="120" />
       <el-table-column
         :formatter="dateFormatter"
         align="center"
-        label="最后跟进时间"
-        prop="contactLastTime"
+        label="更新时间"
+        prop="updateTime"
         width="180px"
       />
       <el-table-column
@@ -104,7 +129,11 @@
         prop="createTime"
         width="180px"
       />
-      <el-table-column align="center" label="备注" prop="remark" />
+      <el-table-column align="center" fixed="right" label="合同状态" prop="auditStatus" width="120">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.CRM_AUDIT_STATUS" :value="scope.row.auditStatus" />
+        </template>
+      </el-table-column>
       <el-table-column fixed="right" label="操作" width="250">
         <template #default="scope">
           <el-button
@@ -115,11 +144,12 @@
           >
             编辑
           </el-button>
+          <!-- TODO @puhui999:可以加下判断,什么情况下,可以审批;然后加个【查看审批】按钮 -->
           <el-button
             v-hasPermi="['crm:contract:update']"
             link
             type="primary"
-            @click="handleApprove(scope.row)"
+            @click="handleSubmit(scope.row)"
           >
             提交审核
           </el-button>
@@ -155,10 +185,12 @@
   <ContractForm ref="formRef" @success="getList" />
 </template>
 <script lang="ts" setup>
-import { dateFormatter } from '@/utils/formatTime'
+import { dateFormatter, dateFormatter2 } from '@/utils/formatTime'
 import download from '@/utils/download'
 import * as ContractApi from '@/api/crm/contract'
 import ContractForm from './ContractForm.vue'
+import { fenToYuanFormat } from '@/utils/formatter'
+import { DICT_TYPE } from '@/utils/dict'
 
 defineOptions({ name: 'CrmContract' })
 
@@ -241,16 +273,29 @@ const handleExport = async () => {
 }
 
 /** 提交审核 **/
-const handleApprove = async (row: ContractApi.ContractVO) => {
+const handleSubmit = async (row: ContractApi.ContractVO) => {
   await message.confirm(`您确定提交【${row.name}】审核吗?`)
-  await ContractApi.handleApprove(row.id)
+  await ContractApi.submitContract(row.id)
   message.success('提交审核成功!')
   await getList()
 }
+
+/** 打开合同详情 */
 const { push } = useRouter()
 const openDetail = (id: number) => {
   push({ name: 'CrmContractDetail', params: { id } })
 }
+
+/** 打开客户详情 */
+const openCustomerDetail = (id: number) => {
+  push({ name: 'CrmCustomerDetail', params: { id } })
+}
+
+/** 打开联系人详情 */
+const openContactDetail = (id: number) => {
+  push({ name: 'CrmContactDetail', params: { id } })
+}
+
 /** 初始化 **/
 onMounted(() => {
   getList()

+ 1 - 0
src/views/crm/contract/oa/ContractDetail/index.vue

@@ -1,3 +1,4 @@
+<!-- TODO @puhui999:这个好像和 detail 重复了???能不能复用 detail 哈? -->
 <template>
   <el-form ref="formRef" v-loading="formLoading" :model="formData" label-width="110px">
     <el-row>

+ 2 - 1
src/views/crm/customer/CustomerImportForm.vue

@@ -1,3 +1,4 @@
+<!-- 客户导入窗口 -->
 <template>
   <Dialog v-model="dialogVisible" title="客户导入" width="400">
     <el-upload
@@ -20,7 +21,7 @@
         <div class="el-upload__tip text-center">
           <div class="el-upload__tip">
             <el-checkbox v-model="updateSupport" />
-            是否更新已经存在的客户数据
+            是否更新已经存在的客户数据(“客户名称”重复)
           </div>
           <span>仅允许导入 xls、xlsx 格式文件。</span>
           <el-link

+ 1 - 1
src/views/crm/customer/detail/index.vue

@@ -124,7 +124,7 @@ const openForm = () => {
 /** 客户转移 */
 const crmTransferFormRef = ref<InstanceType<typeof CrmTransferForm>>() // 客户转移表单 ref
 const transfer = () => {
-  crmTransferFormRef.value?.open('客户转移', customerId.value, CustomerApi.transfer)
+  crmTransferFormRef.value?.open('客户转移', customerId.value, CustomerApi.transferCustomer)
 }
 
 /** 锁定客户 */

+ 6 - 3
src/views/crm/customer/index.vue

@@ -208,7 +208,7 @@
 
   <!-- 表单弹窗:添加/修改 -->
   <CustomerForm ref="formRef" @success="getList" />
-  <CustomerImportForm ref="customerImportFormRef" @success="getList" />
+  <CustomerImportForm ref="importFormRef" @success="getList" />
 </template>
 
 <script lang="ts" setup>
@@ -339,10 +339,13 @@ const handleDelete = async (id: number) => {
     await getList()
   } catch {}
 }
-const customerImportFormRef = ref<InstanceType<typeof CustomerImportForm>>()
+
+/** 导入按钮操作 */
+const importFormRef = ref<InstanceType<typeof CustomerImportForm>>()
 const handleImport = () => {
-  customerImportFormRef.value?.open()
+  importFormRef.value?.open()
 }
+
 /** 导出按钮操作 */
 const handleExport = async () => {
   try {

+ 154 - 0
src/views/erp/product/ProductForm.vue

@@ -0,0 +1,154 @@
+<!-- ERP 产品的新增/修改 -->
+<template>
+  <Dialog :title="dialogTitle" v-model="dialogVisible">
+    <el-form
+      ref="formRef"
+      :model="formData"
+      :rules="formRules"
+      label-width="100px"
+      v-loading="formLoading"
+    >
+      <el-form-item label="名称" prop="name">
+        <el-input v-model="formData.name" placeholder="请输入名称" />
+      </el-form-item>
+      <el-form-item label="条码" prop="barCode">
+        <el-input v-model="formData.barCode" placeholder="请输入条码" />
+      </el-form-item>
+      <el-form-item label="分类编号" prop="categoryId">
+        <el-input v-model="formData.categoryId" placeholder="请输入分类编号" />
+      </el-form-item>
+      <el-form-item label="单位编号" prop="unitId">
+        <el-input v-model="formData.unitId" placeholder="请输入单位编号" />
+      </el-form-item>
+      <el-form-item label="状态" prop="status">
+        <el-radio-group v-model="formData.status">
+          <el-radio label="1">请选择字典生成</el-radio>
+        </el-radio-group>
+      </el-form-item>
+      <el-form-item label="规格" prop="standard">
+        <el-input v-model="formData.standard" placeholder="请输入规格" />
+      </el-form-item>
+      <el-form-item label="备注" prop="remark">
+        <el-input v-model="formData.remark" placeholder="请输入备注" />
+      </el-form-item>
+      <el-form-item label="保质期天数" prop="expiryDay">
+        <el-input v-model="formData.expiryDay" placeholder="请输入保质期天数" />
+      </el-form-item>
+      <el-form-item label="基础重量(kg)" prop="weight">
+        <el-input v-model="formData.weight" placeholder="请输入基础重量(kg)" />
+      </el-form-item>
+      <el-form-item label="采购价格,单位:元" prop="purchasePrice">
+        <el-input v-model="formData.purchasePrice" placeholder="请输入采购价格,单位:元" />
+      </el-form-item>
+      <el-form-item label="销售价格,单位:元" prop="salePrice">
+        <el-input v-model="formData.salePrice" placeholder="请输入销售价格,单位:元" />
+      </el-form-item>
+      <el-form-item label="最低价格,单位:元" prop="minPrice">
+        <el-input v-model="formData.minPrice" placeholder="请输入最低价格,单位:元" />
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+<script setup lang="ts">
+import { ProductApi } from '@/api/erp/product'
+
+/** ERP 产品 表单 */
+defineOptions({ name: 'ProductForm' })
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const dialogTitle = ref('') // 弹窗的标题
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const formType = ref('') // 表单的类型:create - 新增;update - 修改
+const formData = ref({
+  id: undefined,
+  name: undefined,
+  barCode: undefined,
+  categoryId: undefined,
+  unitId: undefined,
+  status: undefined,
+  standard: undefined,
+  remark: undefined,
+  expiryDay: undefined,
+  weight: undefined,
+  purchasePrice: undefined,
+  salePrice: undefined,
+  minPrice: undefined
+})
+const formRules = reactive({
+  name: [{ required: true, message: '产品名称不能为空', trigger: 'blur' }],
+  barCode: [{ required: true, message: '产品条码不能为空', trigger: 'blur' }],
+  categoryId: [{ required: true, message: '产品分类编号不能为空', trigger: 'blur' }],
+  unitId: [{ required: true, message: '单位编号不能为空', trigger: 'blur' }],
+  status: [{ required: true, message: '产品状态不能为空', trigger: 'blur' }]
+})
+const formRef = ref() // 表单 Ref
+
+/** 打开弹窗 */
+const open = async (type: string, id?: number) => {
+  dialogVisible.value = true
+  dialogTitle.value = t('action.' + type)
+  formType.value = type
+  resetForm()
+  // 修改时,设置数据
+  if (id) {
+    formLoading.value = true
+    try {
+      formData.value = await ProductApi.getProduct(id)
+    } finally {
+      formLoading.value = false
+    }
+  }
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  // 校验表单
+  await formRef.value.validate()
+  // 提交请求
+  formLoading.value = true
+  try {
+    const data = formData.value as unknown as ProductApi.ProductVO
+    if (formType.value === 'create') {
+      await ProductApi.createProduct(data)
+      message.success(t('common.createSuccess'))
+    } else {
+      await ProductApi.updateProduct(data)
+      message.success(t('common.updateSuccess'))
+    }
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    id: undefined,
+    name: undefined,
+    barCode: undefined,
+    categoryId: undefined,
+    unitId: undefined,
+    status: undefined,
+    standard: undefined,
+    remark: undefined,
+    expiryDay: undefined,
+    weight: undefined,
+    purchasePrice: undefined,
+    salePrice: undefined,
+    minPrice: undefined
+  }
+  formRef.value?.resetFields()
+}
+</script>

+ 145 - 0
src/views/erp/product/category/ProductCategoryForm.vue

@@ -0,0 +1,145 @@
+<template>
+  <Dialog :title="dialogTitle" v-model="dialogVisible">
+    <el-form
+      ref="formRef"
+      :model="formData"
+      :rules="formRules"
+      label-width="100px"
+      v-loading="formLoading"
+    >
+      <el-form-item label="上级编号" prop="parentId">
+        <el-tree-select
+          v-model="formData.parentId"
+          :data="productCategoryTree"
+          :props="defaultProps"
+          check-strictly
+          default-expand-all
+          placeholder="请选择上级编号"
+        />
+      </el-form-item>
+      <el-form-item label="名称" prop="name">
+        <el-input v-model="formData.name" placeholder="请输入名称" />
+      </el-form-item>
+      <el-form-item label="编码" prop="code">
+        <el-input v-model="formData.code" placeholder="请输入编码" />
+      </el-form-item>
+      <el-form-item label="排序" prop="sort">
+        <el-input v-model="formData.sort" placeholder="请输入排序" />
+      </el-form-item>
+      <el-form-item label="状态" prop="status">
+        <el-radio-group v-model="formData.status">
+          <el-radio
+            v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+            :key="dict.value"
+            :label="dict.value"
+          >
+            {{ dict.label }}
+          </el-radio>
+        </el-radio-group>
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+<script setup lang="ts">
+import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
+import { ProductCategoryApi } from '@/api/erp/product/category'
+import { defaultProps, handleTree } from '@/utils/tree'
+import { CommonStatusEnum } from '@/utils/constants'
+
+/** ERP 产品分类 表单 */
+defineOptions({ name: 'ProductCategoryForm' })
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const dialogTitle = ref('') // 弹窗的标题
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const formType = ref('') // 表单的类型:create - 新增;update - 修改
+const formData = ref({
+  id: undefined,
+  parentId: undefined,
+  name: undefined,
+  code: undefined,
+  sort: undefined,
+  status: undefined
+})
+const formRules = reactive({
+  parentId: [{ required: true, message: '上级编号不能为空', trigger: 'blur' }],
+  name: [{ required: true, message: '名称不能为空', trigger: 'blur' }],
+  code: [{ required: true, message: '编码不能为空', trigger: 'blur' }],
+  sort: [{ required: true, message: '排序不能为空', trigger: 'blur' }],
+  status: [{ required: true, message: '状态不能为空', trigger: 'blur' }]
+})
+const formRef = ref() // 表单 Ref
+const productCategoryTree = ref() // 树形结构
+
+/** 打开弹窗 */
+const open = async (type: string, id?: number) => {
+  dialogVisible.value = true
+  dialogTitle.value = t('action.' + type)
+  formType.value = type
+  resetForm()
+  // 修改时,设置数据
+  if (id) {
+    formLoading.value = true
+    try {
+      formData.value = await ProductCategoryApi.getProductCategory(id)
+    } finally {
+      formLoading.value = false
+    }
+  }
+  await getProductCategoryTree()
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  // 校验表单
+  await formRef.value.validate()
+  // 提交请求
+  formLoading.value = true
+  try {
+    const data = formData.value as unknown as ProductCategoryApi.ProductCategoryVO
+    if (formType.value === 'create') {
+      await ProductCategoryApi.createProductCategory(data)
+      message.success(t('common.createSuccess'))
+    } else {
+      await ProductCategoryApi.updateProductCategory(data)
+      message.success(t('common.updateSuccess'))
+    }
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    id: undefined,
+    parentId: undefined,
+    name: undefined,
+    code: undefined,
+    sort: undefined,
+    status: CommonStatusEnum.ENABLE
+  }
+  formRef.value?.resetFields()
+}
+
+/** 获得产品分类树 */
+const getProductCategoryTree = async () => {
+  productCategoryTree.value = []
+  const data = await ProductCategoryApi.getProductCategoryList()
+  const root: Tree = { id: 0, name: '顶级产品分类', children: [] }
+  root.children = handleTree(data, 'id', 'parentId')
+  productCategoryTree.value.push(root)
+}
+</script>

+ 220 - 0
src/views/erp/product/category/index.vue

@@ -0,0 +1,220 @@
+<template>
+  <ContentWrap>
+    <!-- 搜索工作栏 -->
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="68px"
+    >
+      <el-form-item label="分类名称" prop="name">
+        <el-input
+          v-model="queryParams.name"
+          placeholder="请输入分类名称"
+          clearable
+          @keyup.enter="handleQuery"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item label="开启状态" prop="status">
+        <el-select
+          v-model="queryParams.status"
+          placeholder="请选择开启状态"
+          clearable
+          class="!w-240px"
+        >
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item>
+        <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
+        <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
+        <el-button
+          type="primary"
+          plain
+          @click="openForm('create')"
+          v-hasPermi="['erp:product-category:create']"
+        >
+          <Icon icon="ep:plus" class="mr-5px" /> 新增
+        </el-button>
+        <el-button
+          type="success"
+          plain
+          @click="handleExport"
+          :loading="exportLoading"
+          v-hasPermi="['erp:product-category:export']"
+        >
+          <Icon icon="ep:download" class="mr-5px" /> 导出
+        </el-button>
+        <el-button type="danger" plain @click="toggleExpandAll">
+          <Icon icon="ep:sort" class="mr-5px" /> 展开/折叠
+        </el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-table
+      v-loading="loading"
+      :data="list"
+      :stripe="true"
+      :show-overflow-tooltip="true"
+      row-key="id"
+      :default-expand-all="isExpandAll"
+      v-if="refreshTable"
+    >
+      <el-table-column label="编码" align="center" prop="code" />
+      <el-table-column label="名称" align="center" prop="name" />
+      <el-table-column label="排序" align="center" prop="sort" />
+      <el-table-column label="状态" align="center" prop="status">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
+        </template>
+      </el-table-column>
+      <el-table-column
+        label="创建时间"
+        align="center"
+        prop="createTime"
+        :formatter="dateFormatter"
+        width="180px"
+      />
+      <el-table-column label="操作" align="center">
+        <template #default="scope">
+          <el-button
+            link
+            type="primary"
+            @click="openForm('update', scope.row.id)"
+            v-hasPermi="['erp:product-category:update']"
+          >
+            编辑
+          </el-button>
+          <el-button
+            link
+            type="danger"
+            @click="handleDelete(scope.row.id)"
+            v-hasPermi="['erp:product-category:delete']"
+          >
+            删除
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </ContentWrap>
+
+  <!-- 表单弹窗:添加/修改 -->
+  <ProductCategoryForm ref="formRef" @success="getList" />
+</template>
+
+<script setup lang="ts">
+import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import { handleTree } from '@/utils/tree'
+import download from '@/utils/download'
+import { ProductCategoryApi, ProductCategoryVO } from '@/api/erp/product/category'
+import ProductCategoryForm from './ProductCategoryForm.vue'
+
+/** ERP 产品分类 列表 */
+defineOptions({ name: 'ErpProductCategory' })
+
+const message = useMessage() // 消息弹窗
+const { t } = useI18n() // 国际化
+
+const loading = ref(true) // 列表的加载中
+const list = ref<ProductCategoryVO[]>([]) // 列表的数据
+const queryParams = reactive({
+  parentId: undefined,
+  name: undefined,
+  code: undefined,
+  sort: undefined,
+  status: undefined,
+  createTime: []
+})
+const queryFormRef = ref() // 搜索的表单
+const exportLoading = ref(false) // 导出的加载中
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await ProductCategoryApi.getProductCategoryList(queryParams)
+    list.value = handleTree(data, 'id', 'parentId')
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value.resetFields()
+  handleQuery()
+}
+
+/** 添加/修改操作 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+  formRef.value.open(type, id)
+}
+
+/** 删除按钮操作 */
+const handleDelete = async (id: number) => {
+  try {
+    // 删除的二次确认
+    await message.delConfirm()
+    // 发起删除
+    await ProductCategoryApi.deleteProductCategory(id)
+    message.success(t('common.delSuccess'))
+    // 刷新列表
+    await getList()
+  } catch {}
+}
+
+/** 导出按钮操作 */
+const handleExport = async () => {
+  try {
+    // 导出的二次确认
+    await message.exportConfirm()
+    // 发起导出
+    exportLoading.value = true
+    const data = await ProductCategoryApi.exportProductCategory(queryParams)
+    download.excel(data, '产品分类.xls')
+  } catch {
+  } finally {
+    exportLoading.value = false
+  }
+}
+
+/** 展开/折叠操作 */
+const isExpandAll = ref(true) // 是否展开,默认全部展开
+const refreshTable = ref(true) // 重新渲染表格状态
+const toggleExpandAll = async () => {
+  refreshTable.value = false
+  isExpandAll.value = !isExpandAll.value
+  await nextTick()
+  refreshTable.value = true
+}
+
+/** 初始化 **/
+onMounted(() => {
+  getList()
+})
+</script>

+ 208 - 0
src/views/erp/product/index.vue

@@ -0,0 +1,208 @@
+<!-- ERP 产品列表 -->
+<template>
+  <ContentWrap>
+    <!-- 搜索工作栏 -->
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="68px"
+    >
+      <el-form-item label="产品名称" prop="name">
+        <el-input
+          v-model="queryParams.name"
+          placeholder="请输入产品名称"
+          clearable
+          @keyup.enter="handleQuery"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item label="产品分类" prop="categoryId">
+        <el-input
+          v-model="queryParams.categoryId"
+          placeholder="请输入产品分类"
+          clearable
+          @keyup.enter="handleQuery"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item label="创建时间" prop="createTime">
+        <el-date-picker
+          v-model="queryParams.createTime"
+          value-format="YYYY-MM-DD HH:mm:ss"
+          type="daterange"
+          start-placeholder="开始日期"
+          end-placeholder="结束日期"
+          :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item>
+        <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
+        <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
+        <el-button
+          type="primary"
+          plain
+          @click="openForm('create')"
+          v-hasPermi="['erp:product:create']"
+        >
+          <Icon icon="ep:plus" class="mr-5px" /> 新增
+        </el-button>
+        <el-button
+          type="success"
+          plain
+          @click="handleExport"
+          :loading="exportLoading"
+          v-hasPermi="['erp:product:export']"
+        >
+          <Icon icon="ep:download" class="mr-5px" /> 导出
+        </el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+      <el-table-column label="条码" align="center" prop="barCode" />
+      <el-table-column label="名称" align="center" prop="name" />
+      <el-table-column label="规格" align="center" prop="standard" />
+      <!-- TODO 芋艿:待实现 -->
+      <el-table-column label="分类" align="center" prop="categoryId" />
+      <!-- TODO 芋艿:待实现 -->
+      <el-table-column label="单位" align="center" prop="unitId" />
+      <el-table-column label="采购价格" align="center" prop="purchasePrice" />
+      <el-table-column label="销售价格" align="center" prop="salePrice" />
+      <el-table-column label="最低价格" align="center" prop="minPrice" />
+      <!-- TODO 芋艿:待实现 -->
+      <el-table-column label="产品状态" align="center" prop="status" />
+      <el-table-column
+        label="创建时间"
+        align="center"
+        prop="createTime"
+        :formatter="dateFormatter"
+        width="180px"
+      />
+      <el-table-column label="操作" align="center" width="110">
+        <template #default="scope">
+          <el-button
+            link
+            type="primary"
+            @click="openForm('update', scope.row.id)"
+            v-hasPermi="['erp:product:update']"
+          >
+            编辑
+          </el-button>
+          <el-button
+            link
+            type="danger"
+            @click="handleDelete(scope.row.id)"
+            v-hasPermi="['erp:product:delete']"
+          >
+            删除
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </ContentWrap>
+
+  <!-- 表单弹窗:添加/修改 -->
+  <ProductForm ref="formRef" @success="getList" />
+</template>
+
+<script setup lang="ts">
+import { dateFormatter } from '@/utils/formatTime'
+import download from '@/utils/download'
+import { ProductApi, ProductVO } from '@/api/erp/product'
+import ProductForm from './ProductForm.vue'
+
+/** ERP 产品列表 */
+defineOptions({ name: 'ErpProduct' })
+
+const message = useMessage() // 消息弹窗
+const { t } = useI18n() // 国际化
+
+const loading = ref(true) // 列表的加载中
+const list = ref<ProductVO[]>([]) // 列表的数据
+const total = ref(0) // 列表的总页数
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  name: undefined,
+  categoryId: undefined,
+  createTime: []
+})
+const queryFormRef = ref() // 搜索的表单
+const exportLoading = ref(false) // 导出的加载中
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await ProductApi.getProductPage(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value.resetFields()
+  handleQuery()
+}
+
+/** 添加/修改操作 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+  formRef.value.open(type, id)
+}
+
+/** 删除按钮操作 */
+const handleDelete = async (id: number) => {
+  try {
+    // 删除的二次确认
+    await message.delConfirm()
+    // 发起删除
+    await ProductApi.deleteProduct(id)
+    message.success(t('common.delSuccess'))
+    // 刷新列表
+    await getList()
+  } catch {}
+}
+
+/** 导出按钮操作 */
+const handleExport = async () => {
+  try {
+    // 导出的二次确认
+    await message.exportConfirm()
+    // 发起导出
+    exportLoading.value = true
+    const data = await ProductApi.exportProduct(queryParams)
+    download.excel(data, '产品.xls')
+  } catch {
+  } finally {
+    exportLoading.value = false
+  }
+}
+
+/** 初始化 **/
+onMounted(() => {
+  getList()
+})
+</script>

+ 100 - 0
src/views/erp/product/unit/ProductUnitForm.vue

@@ -0,0 +1,100 @@
+<template>
+  <Dialog :title="dialogTitle" v-model="dialogVisible">
+    <el-form
+      ref="formRef"
+      :model="formData"
+      :rules="formRules"
+      label-width="100px"
+      v-loading="formLoading"
+    >
+      <el-form-item label="单位名字" prop="name">
+        <el-input v-model="formData.name" placeholder="请输入单位名字" />
+      </el-form-item>
+      <el-form-item label="单位状态" prop="status">
+        <el-radio-group v-model="formData.status">
+          <el-radio label="1">请选择字典生成</el-radio>
+        </el-radio-group>
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+<script setup lang="ts">
+import { ProductUnitApi } from '@/api/erp/product/unit'
+
+/** ERP 产品单位表单 */
+defineOptions({ name: 'ProductUnitForm' })
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const dialogTitle = ref('') // 弹窗的标题
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const formType = ref('') // 表单的类型:create - 新增;update - 修改
+const formData = ref({
+  id: undefined,
+  name: undefined,
+  status: undefined
+})
+const formRules = reactive({
+  name: [{ required: true, message: '单位名字不能为空', trigger: 'blur' }],
+  status: [{ required: true, message: '单位状态不能为空', trigger: 'blur' }]
+})
+const formRef = ref() // 表单 Ref
+
+/** 打开弹窗 */
+const open = async (type: string, id?: number) => {
+  dialogVisible.value = true
+  dialogTitle.value = t('action.' + type)
+  formType.value = type
+  resetForm()
+  // 修改时,设置数据
+  if (id) {
+    formLoading.value = true
+    try {
+      formData.value = await ProductUnitApi.getProductUnit(id)
+    } finally {
+      formLoading.value = false
+    }
+  }
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  // 校验表单
+  await formRef.value.validate()
+  // 提交请求
+  formLoading.value = true
+  try {
+    const data = formData.value as unknown as ProductUnitApi.ProductUnitVO
+    if (formType.value === 'create') {
+      await ProductUnitApi.createProductUnit(data)
+      message.success(t('common.createSuccess'))
+    } else {
+      await ProductUnitApi.updateProductUnit(data)
+      message.success(t('common.updateSuccess'))
+    }
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    id: undefined,
+    name: undefined,
+    status: undefined
+  }
+  formRef.value?.resetFields()
+}
+</script>

+ 200 - 0
src/views/erp/product/unit/index.vue

@@ -0,0 +1,200 @@
+<template>
+  <ContentWrap>
+    <!-- 搜索工作栏 -->
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="68px"
+    >
+      <el-form-item label="单位名字" prop="name">
+        <el-input
+          v-model="queryParams.name"
+          placeholder="请输入单位名字"
+          clearable
+          @keyup.enter="handleQuery"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item label="单位状态" prop="status">
+        <el-select
+          v-model="queryParams.status"
+          placeholder="请选择单位状态"
+          clearable
+          class="!w-240px"
+        >
+          <el-option label="请选择字典生成" value="" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="创建时间" prop="createTime">
+        <el-date-picker
+          v-model="queryParams.createTime"
+          value-format="YYYY-MM-DD HH:mm:ss"
+          type="daterange"
+          start-placeholder="开始日期"
+          end-placeholder="结束日期"
+          :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item>
+        <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
+        <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
+        <el-button
+          type="primary"
+          plain
+          @click="openForm('create')"
+          v-hasPermi="['erp:product-unit:create']"
+        >
+          <Icon icon="ep:plus" class="mr-5px" /> 新增
+        </el-button>
+        <el-button
+          type="success"
+          plain
+          @click="handleExport"
+          :loading="exportLoading"
+          v-hasPermi="['erp:product-unit:export']"
+        >
+          <Icon icon="ep:download" class="mr-5px" /> 导出
+        </el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+      <el-table-column label="单位编号" align="center" prop="id" />
+      <el-table-column label="单位名字" align="center" prop="name" />
+      <el-table-column label="单位状态" align="center" prop="status" />
+      <el-table-column
+        label="创建时间"
+        align="center"
+        prop="createTime"
+        :formatter="dateFormatter"
+        width="180px"
+      />
+      <el-table-column label="操作" align="center">
+        <template #default="scope">
+          <el-button
+            link
+            type="primary"
+            @click="openForm('update', scope.row.id)"
+            v-hasPermi="['erp:product-unit:update']"
+          >
+            编辑
+          </el-button>
+          <el-button
+            link
+            type="danger"
+            @click="handleDelete(scope.row.id)"
+            v-hasPermi="['erp:product-unit:delete']"
+          >
+            删除
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </ContentWrap>
+
+  <!-- 表单弹窗:添加/修改 -->
+  <ProductUnitForm ref="formRef" @success="getList" />
+</template>
+
+<script setup lang="ts">
+import { dateFormatter } from '@/utils/formatTime'
+import download from '@/utils/download'
+import { ProductUnitApi, ProductUnitVO } from '@/api/erp/product/unit'
+import ProductUnitForm from './ProductUnitForm.vue'
+
+/** ERP 产品单位列表 */
+defineOptions({ name: 'ErpProductUnit' })
+
+const message = useMessage() // 消息弹窗
+const { t } = useI18n() // 国际化
+
+const loading = ref(true) // 列表的加载中
+const list = ref<ProductUnitVO[]>([]) // 列表的数据
+// 列表的总页数
+const total = ref(0)
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  name: undefined,
+  status: undefined,
+  createTime: []
+})
+const queryFormRef = ref() // 搜索的表单
+const exportLoading = ref(false) // 导出的加载中
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await ProductUnitApi.getProductUnitPage(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value.resetFields()
+  handleQuery()
+}
+
+/** 添加/修改操作 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+  formRef.value.open(type, id)
+}
+
+/** 删除按钮操作 */
+const handleDelete = async (id: number) => {
+  try {
+    // 删除的二次确认
+    await message.delConfirm()
+    // 发起删除
+    await ProductUnitApi.deleteProductUnit(id)
+    message.success(t('common.delSuccess'))
+    // 刷新列表
+    await getList()
+  } catch {}
+}
+
+/** 导出按钮操作 */
+const handleExport = async () => {
+  try {
+    // 导出的二次确认
+    await message.exportConfirm()
+    // 发起导出
+    exportLoading.value = true
+    const data = await ProductUnitApi.exportProductUnit(queryParams)
+    download.excel(data, 'ERP 产品单位.xls')
+  } catch {
+  } finally {
+    exportLoading.value = false
+  }
+}
+
+/** 初始化 **/
+onMounted(() => {
+  getList()
+})
+</script>