From 7ae546c24b64136c56cb2a33ad79b235f20eb603 Mon Sep 17 00:00:00 2001
From: gaoluyang <2820782392@qq.com>
Date: 星期五, 12 十二月 2025 10:56:25 +0800
Subject: [PATCH] 1.公司-添加商机管理页面

---
 src/views/salesManagement/opportunityManagement/index.vue |  574 ++++++++++++++++++++++++++++++++++++++++++++++++++++
 src/api/salesManagement/opportunityManagement.js          |   38 +++
 2 files changed, 612 insertions(+), 0 deletions(-)

diff --git a/src/api/salesManagement/opportunityManagement.js b/src/api/salesManagement/opportunityManagement.js
new file mode 100644
index 0000000..b989150
--- /dev/null
+++ b/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,
+  });
+}
\ No newline at end of file
diff --git a/src/views/salesManagement/opportunityManagement/index.vue b/src/views/salesManagement/opportunityManagement/index.vue
new file mode 100644
index 0000000..170830b
--- /dev/null
+++ b/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
+  
+  // 鍒涘缓鏌ヨ鍙傛暟锛屾帓闄ntryDate瀛楁锛屽彧浣跨敤entryDateStart鍜宔ntryDateEnd
+  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>
\ No newline at end of file

--
Gitblit v1.9.3