gaoluyang
2025-12-12 7ae546c24b64136c56cb2a33ad79b235f20eb603
1.公司-添加商机管理页面
已添加2个文件
612 ■■■■■ 文件已修改
src/api/salesManagement/opportunityManagement.js 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/opportunityManagement/index.vue 574 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/salesManagement/opportunityManagement.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,38 @@
// å•†æœºç®¡ç†æŽ¥å£
import request from "@/utils/request";
// åˆ†é¡µæŸ¥è¯¢å•†æœºåˆ—表
export function opportunityListPage(query) {
  return request({
    url: "/businessOpportunity/listPage",
    method: "get",
    params: query,
  });
}
// æ–°å¢žå•†æœº
export function addOpportunity(data) {
  return request({
    url: "/businessOpportunity/add",
    method: "post",
    data: data,
  });
}
// ä¿®æ”¹å•†æœº
export function updateOpportunity(data) {
  return request({
    url: "/businessOpportunity/update",
    method: "post",
    data: data,
  });
}
// åˆ é™¤å•†æœº
export function delOpportunity(ids) {
  return request({
    url: "/businessOpportunity/delete",
    method: "delete",
    data: ids,
  });
}
src/views/salesManagement/opportunityManagement/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,574 @@
<template>
  <div class="app-container">
    <!-- æœç´¢åŒºåŸŸ -->
    <div class="search_form">
      <el-form :model="searchForm" :inline="true" label-width="auto">
        <el-form-item label="客户名称">
          <el-input
            v-model="searchForm.customerName"
            placeholder="请输入客户名称"
            clearable
            prefix-icon="Search"
            style="width: 200px"
            @change="handleQuery"
          />
        </el-form-item>
        <el-form-item label="录入日期:">
          <el-date-picker v-model="searchForm.entryDate" value-format="YYYY-MM-DD" format="YYYY-MM-DD" type="daterange"
            placeholder="请选择" clearable @change="changeDaterange" />
        </el-form-item>
        <el-form-item>
          <el-button type="primary" @click="handleQuery">搜索</el-button>
          <el-button @click="resetQuery">重置</el-button>
        </el-form-item>
      </el-form>
            <div class="actions">
            <el-button type="primary" @click="handleAdd">新建</el-button>
            <el-button type="danger" plain @click="handleDelete">删除</el-button>
        </div>
    </div>
    <!-- è¡¨æ ¼åŒºåŸŸ -->
    <div class="table_list">
      <el-table
        :data="tableData"
        border
        v-loading="tableLoading"
        @selection-change="handleSelectionChange"
        :row-key="(row) => row.id"
        height="calc(100vh - 18.5em)"
        stripe
      >
        <el-table-column align="center" type="selection" width="55" />
        <el-table-column align="center" label="序号" type="index" width="60" />
        <el-table-column label="状态" prop="status" width="120">
          <template #default="{ row }">
            <el-tag
              :type="getStatusTagType(row.status)"
              effect="light"
            >
              {{ getStatusText(row.status) }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column label="省份" prop="province" show-overflow-tooltip width="120" />
        <el-table-column label="客户名称" prop="customerName" show-overflow-tooltip width="230" />
        <el-table-column label="商机来源" prop="businessSource" show-overflow-tooltip width="150" />
        <el-table-column label="客户描述" prop="description" show-overflow-tooltip min-width="200" />
        <el-table-column label="录入人" prop="entryPerson" show-overflow-tooltip width="120" />
        <el-table-column label="更新日期" prop="updateTime" width="120">
          <template #default="{ row }">
            {{ formatDate(row.updateTime) }}
          </template>
        </el-table-column>
        <el-table-column label="操作" fixed="right" width="130" align="center">
          <template #default="{ row }">
            <el-button
              link
              type="primary"
              size="small"
              @click="handleEdit(row)"
            >
              ç¼–辑
            </el-button>
            <el-button
              link
              type="primary"
              size="small"
              @click="handleDetail(row)"
            >
              è¯¦æƒ…
            </el-button>
          </template>
        </el-table-column>
      </el-table>
      <!-- åˆ†é¡µç»„ä»¶ -->
      <pagination
        v-show="total > 0"
        :total="total"
        layout="total, sizes, prev, pager, next, jumper"
        :page="page.current"
        :limit="page.size"
        @pagination="paginationChange"
      />
    </div>
    <!-- æ–°å¢ž/编辑对话框 -->
    <el-dialog
      v-model="dialogFormVisible"
      :title="operationType === 'add' ? '新建商机' : operationType === 'edit' ? '编辑商机' : '商机详情'"
      width="600px"
      @close="closeDialog"
    >
      <el-form
        :model="form"
        :rules="rules"
        ref="formRef"
        label-width="100px"
        label-position="left"
      >
        <el-form-item label="状态" prop="status">
          <el-select v-model="form.status" placeholder="请选择状态" style="width: 100%" :disabled="operationType === 'detail'">
            <el-option
              v-for="item in statusOptions"
              :key="item.value"
              :label="item.label"
              :value="item.value"
            />
          </el-select>
        </el-form-item>
        <el-form-item label="省份" prop="province">
          <el-select v-model="form.province" filterable placeholder="请选择省份" style="width: 100%" :disabled="operationType === 'detail'">
            <el-option
              v-for="item in provinceOptions"
              :key="item.value"
              :label="item.label"
              :value="item.value"
            />
          </el-select>
        </el-form-item>
        <el-form-item label="客户名称" prop="customerName">
          <el-select v-model="form.customerName" placeholder="请选择" clearable :disabled="operationType === 'detail'">
            <el-option v-for="item in customerOption" :key="item.customerName" :label="item.customerName" :value="item.customerName">
              {{
                item.customerName + "——" + item.taxpayerIdentificationNumber
              }}
            </el-option>
          </el-select>
        </el-form-item>
        <el-form-item label="商机来源" prop="businessSource">
          <el-input v-model="form.businessSource" placeholder="请输入商机来源" :disabled="operationType === 'detail'" />
        </el-form-item>
        <el-form-item label="客户描述" prop="description">
          <el-input
            v-model="form.description"
            type="textarea"
            :rows="3"
            placeholder="请输入客户描述"
            maxlength="500"
            show-word-limit
            :disabled="operationType === 'detail'"
          />
        </el-form-item>
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="录入人" prop="entryPerson">
              <el-select v-model="form.entryPerson" placeholder="请选择" clearable @change="changs" :disabled="operationType === 'detail'">
                <el-option v-for="item in userList" :key="item.nickName" :label="item.nickName" :value="item.nickName" />
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="录入日期" prop="entryDateStart">
              <el-date-picker style="width: 100%" v-model="form.entryDateStart" value-format="YYYY-MM-DD" format="YYYY-MM-DD"
                type="date" placeholder="请选择" clearable :disabled="operationType === 'detail'" />
            </el-form-item>
          </el-col>
        </el-row>
      </el-form>
      <template #footer>
        <div class="dialog-footer">
          <el-button @click="closeDialog">取消</el-button>
          <el-button type="primary" @click="submitForm" v-if="operationType !== 'detail'">
            ç¡®å®š
          </el-button>
        </div>
      </template>
    </el-dialog>
  </div>
</template>
<script setup>
import { ref, reactive, onMounted, getCurrentInstance } from 'vue'
import { Search } from '@element-plus/icons-vue'
import { ElMessageBox, ElMessage } from 'element-plus'
import pagination from '@/components/PIMTable/Pagination.vue'
import useUserStore from '@/store/modules/user'
import dayjs from 'dayjs'
import {
  opportunityListPage,
  addOpportunity,
  updateOpportunity,
  delOpportunity
} from '@/api/salesManagement/opportunityManagement.js'
import { userListNoPage } from '@/api/system/user.js'
import { customerList } from '@/api/salesManagement/salesLedger.js'
const { proxy } = getCurrentInstance()
const userStore = useUserStore()
// è¡¨æ ¼æ•°æ®
const tableData = ref([])
const selectedRows = ref([])
const tableLoading = ref(false)
const userList = ref([])
const customerOption = ref([])
// åˆ†é¡µé…ç½®
const page = reactive({
  current: 1,
  size: 100,
})
const total = ref(0)
// æœç´¢è¡¨å•
const searchForm = reactive({
  customerName: '',
  entryDate: [],
  entryDateStart: '',
  entryDateEnd: ''
})
// å¯¹è¯æ¡†ç›¸å…³
const dialogFormVisible = ref(false)
const operationType = ref('') // add, detail
const formRef = ref()
const form = reactive({
  id: undefined,
  status: 'new',
  province: '',
  customerName: '',
  businessSource: '',
  description: '',
  entryPerson: userStore.nickName,
  entryDateStart: dayjs().format('YYYY-MM-DD')
})
// è¡¨å•验证规则
const rules = reactive({
  customerName: [
    { required: true, message: '请选择客户', trigger: 'change' }
  ],
  status: [
    { required: true, message: '请选择状态', trigger: 'change' }
  ],
  entryPerson: [
    { required: true, message: '请选择录入人', trigger: 'change' }
  ],
  entryDateStart: [
    { required: true, message: '请选择录入日期', trigger: 'change' }
  ]
})
// çŠ¶æ€é€‰é¡¹
const statusOptions = [
  { value: 'new', label: '新建' },
  { value: 'tracking', label: '项目跟踪' },
  { value: 'contract', label: '合同签约' },
  { value: 'delivery', label: '项目交付' },
  { value: 'acceptance', label: '项目验收' }
]
// çœä»½é€‰é¡¹ï¼ˆç¤ºä¾‹ï¼‰
const provinceOptions = [
  { value: 'beijing', label: '北京市' },
  { value: 'tianjin', label: '天津市' },
  { value: 'hebei', label: '河北省' },
  { value: 'shanxi', label: '山西省' },
  { value: 'neimenggu', label: '内蒙古自治区' },
  { value: 'liaoning', label: '辽宁省' },
  { value: 'jilin', label: '吉林省' },
  { value: 'heilongjiang', label: '黑龙江省' },
  { value: 'shanghai', label: '上海市' },
  { value: 'jiangsu', label: '江苏省' },
  { value: 'zhejiang', label: '浙江省' },
  { value: 'anhui', label: '安徽省' },
  { value: 'fujian', label: '福建省' },
  { value: 'jiangxi', label: '江西省' },
  { value: 'shandong', label: '山东省' },
  { value: 'henan', label: '河南省' },
  { value: 'hubei', label: '湖北省' },
  { value: 'hunan', label: '湖南省' },
  { value: 'guangdong', label: '广东省' },
  { value: 'guangxi', label: '广西壮族自治区' },
  { value: 'hainan', label: '海南省' },
  { value: 'chongqing', label: '重庆市' },
  { value: 'sichuan', label: '四川省' },
  { value: 'guizhou', label: '贵州省' },
  { value: 'yunnan', label: '云南省' },
  { value: 'xizang', label: '西藏自治区' },
  { value: 'shaanxi', label: '陕西省' },
  { value: 'gansu', label: '甘肃省' },
  { value: 'qinghai', label: '青海省' },
  { value: 'ningxia', label: '宁夏回族自治区' },
  { value: 'xinjiang', label: '新疆维吾尔自治区' },
  { value: 'taiwan', label: '台湾省' },
  { value: 'xianggang', label: '香港特别行政区' },
  { value: 'aomen', label: '澳门特别行政区' }
]
// èŽ·å–çŠ¶æ€æ ‡ç­¾ç±»åž‹
const getStatusTagType = (status) => {
  const typeMap = {
    'new': 'info',
    'tracking': 'primary',
    'contract': 'warning',
    'delivery': 'success',
    'acceptance': 'success'
  }
  return typeMap[status] || 'info'
}
// èŽ·å–çŠ¶æ€æ–‡æœ¬
const getStatusText = (status) => {
  const textMap = {
    'new': '新建',
    'tracking': '项目跟踪',
    'contract': '合同签约',
    'delivery': '项目交付',
    'acceptance': '项目验收'
  }
  return textMap[status] || '未知'
}
// æ ¼å¼åŒ–日期
const formatDate = (date) => {
  if (!date) return ''
  return dayjs(date).format('YYYY-MM-DD')
}
// æŸ¥è¯¢åˆ—表
const handleQuery = () => {
  page.current = 1
  getList()
}
// é‡ç½®æŸ¥è¯¢
const resetQuery = () => {
  Object.assign(searchForm, {
    customerName: '',
    entryDate: [],
    entryDateStart: '',
    entryDateEnd: ''
  })
  handleQuery()
}
// æ—¥æœŸèŒƒå›´å˜åŒ–
const changeDaterange = (val) => {
  if (val && val.length === 2) {
    searchForm.entryDateStart = val[0]
    searchForm.entryDateEnd = val[1]
  } else {
    searchForm.entryDateStart = ''
    searchForm.entryDateEnd = ''
  }
  handleQuery()
}
// èŽ·å–åˆ—è¡¨æ•°æ®
const getList = () => {
  tableLoading.value = true
  // åˆ›å»ºæŸ¥è¯¢å‚数,排除entryDate字段,只使用entryDateStart和entryDateEnd
  const { entryDate, ...queryParams } = searchForm
  const params = {
    ...queryParams,
    ...page
  }
  // åˆ é™¤ç©ºå€¼å‚æ•°
  Object.keys(params).forEach(key => {
    if (params[key] === '' || params[key] === null || params[key] === undefined) {
      delete params[key]
    }
  })
  opportunityListPage(params).then(res => {
    tableData.value = res.data.records || []
    total.value = res.data.total || 0
  }).catch(err => {
    console.error('获取商机列表失败:', err)
    tableData.value = []
    total.value = 0
  }).finally(() => {
    tableLoading.value = false
  })
}
// åˆ†é¡µå˜åŒ–
const paginationChange = (pagination) => {
  page.current = pagination.page
  page.size = pagination.limit
  getList()
}
// é€‰æ‹©å˜åŒ–
const handleSelectionChange = (selection) => {
  selectedRows.value = selection
}
// æ–°å»ºå•†æœº
const handleAdd = async () => {
  operationType.value = 'add'
  resetForm()
  // åŠ è½½ç”¨æˆ·åˆ—è¡¨å’Œå®¢æˆ·åˆ—è¡¨
  let userLists = await userListNoPage()
  userList.value = userLists.data
  customerList().then((res) => {
    customerOption.value = res
  })
  dialogFormVisible.value = true
}
// æŸ¥çœ‹è¯¦æƒ…
const handleDetail = async (row) => {
  operationType.value = 'detail'
  // åŠ è½½ç”¨æˆ·åˆ—è¡¨å’Œå®¢æˆ·åˆ—è¡¨
  let userLists = await userListNoPage()
  userList.value = userLists.data
  customerList().then((res) => {
    customerOption.value = res
  })
  // ä½¿ç”¨updateTime作为录入时间反显
  Object.assign(form, row, {
    entryDateStart: row.updateTime || row.entryDateStart
  })
  dialogFormVisible.value = true
}
// ç¼–辑商机
const handleEdit = async (row) => {
  operationType.value = 'edit'
  // åŠ è½½ç”¨æˆ·åˆ—è¡¨å’Œå®¢æˆ·åˆ—è¡¨
  let userLists = await userListNoPage()
  userList.value = userLists.data
  customerList().then((res) => {
    customerOption.value = res
  })
  // ä½¿ç”¨updateTime作为录入时间反显
  Object.assign(form, row, {
    entryDateStart: row.updateTime || row.entryDateStart
  })
  dialogFormVisible.value = true
}
// å½•入人变化处理
const changs = (value) => {
  // å¯ä»¥æ ¹æ®éœ€è¦æ·»åŠ å¤„ç†é€»è¾‘
}
// æäº¤è¡¨å•
const submitForm = () => {
  formRef.value.validate(valid => {
    if (valid) {
      const api = operationType.value === 'add' ? addOpportunity : updateOpportunity
      api(form).then(res => {
        if (res.code === 200) {
          proxy.$modal.msgSuccess(operationType.value === 'add' ? '新建成功' : '修改成功')
          closeDialog()
          getList()
        } else {
          proxy.$modal.msgError(res.msg || '操作失败')
        }
      }).catch(err => {
        proxy.$modal.msgError('操作失败')
      })
    }
  })
}
// åˆ é™¤å•†æœº
const handleDelete = () => {
  if (selectedRows.value.length === 0) {
    proxy.$modal.msgWarning('请选择要删除的商机')
    return
  }
  ElMessageBox.confirm('确定删除选中的商机吗?', '提示', {
    confirmButtonText: '确定',
    cancelButtonText: '取消',
    type: 'warning'
  }).then(() => {
    const ids = selectedRows.value.map(item => item.id)
    delOpportunity(ids).then(res => {
      if (res.code === 200) {
        proxy.$modal.msgSuccess('删除成功')
        getList()
      } else {
        proxy.$modal.msgError(res.msg || '删除失败')
      }
    }).catch(err => {
      proxy.$modal.msgError('删除失败')
    })
  }).catch(() => {
    // ç”¨æˆ·å–消删除
  })
}
// é‡ç½®è¡¨å•
const resetForm = () => {
  Object.assign(form, {
    id: undefined,
    status: 'new',
    province: '',
    customerName: '',
    businessSource: '',
    description: '',
    entryPerson: userStore.nickName,
    entryDateStart: dayjs().format('YYYY-MM-DD')
  })
  if (formRef.value) {
    formRef.value.clearValidate()
  }
}
// å…³é—­å¯¹è¯æ¡†
const closeDialog = () => {
  dialogFormVisible.value = false
  resetForm()
}
onMounted(() => {
  getList()
})
</script>
<style scoped lang="scss">
.app-container {
  padding: 20px;
}
.search_form {
  display: flex;
  align-items: flex-start;
    justify-content: space-between;
}
.table_list {
  margin-top: unset;
}
.dialog-footer {
  text-align: right;
}
:deep(.el-form-item__label) {
  font-weight: 500;
}
:deep(.el-table) {
  .el-table__header-wrapper {
    th {
      background-color: #f0f2f5;
      color: #333;
      font-weight: 600;
    }
  }
}
</style>