ba1f0f8cf67c4419288e3197d61f0d15e139d904..e8eda23ee6fe0273619d826ba768ce975a0d8ccf
2025-12-12 gaoluyang
1.公司-添加商机管理页面
e8eda2 对比 | 目录
2025-12-12 gaoluyang
1.公司-添加商机管理页面
7ae546 对比 | 目录
已添加3个文件
已修改2个文件
977 ■■■■■ 文件已修改
src/api/salesManagement/opportunityManagement.js 46 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionReporting/components/formDia.vue 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/opportunityManagement/fileList.vue 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/opportunityManagement/index.vue 883 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
vite.config.js 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/salesManagement/opportunityManagement.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,46 @@
// å•†æœºç®¡ç†æŽ¥å£
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 addDescription(data) {
  return request({
    url: "/businessOpportunity/addDescription",
    method: "post",
    data: data,
  });
}
// åˆ é™¤å•†æœº
export function delOpportunity(ids) {
  return request({
    url: "/businessOpportunity/delete",
    method: "delete",
    data: ids,
  });
}
src/views/productionManagement/productionReporting/components/formDia.vue
@@ -42,6 +42,9 @@
                                v-model="form.schedulingUserId"
                                placeholder="选择人员"
                                style="width: 100%;"
                filterable
                default-first-option
                :reserve-keyword="false"
                            >
                                <el-option
                                    v-for="user in userList"
src/views/salesManagement/opportunityManagement/fileList.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,43 @@
<template>
  <el-dialog v-model="dialogVisible" title="附件" width="40%" :before-close="handleClose">
    <el-table :data="tableData" border height="40vh" stripe>
      <el-table-column label="附件名称" prop="name" min-width="400" show-overflow-tooltip />
      <el-table-column fixed="right" label="操作" width="100" align="center">
        <template #default="scope">
          <el-button link type="primary" size="small" @click="downLoadFile(scope.row)">下载</el-button>
          <el-button link type="primary" size="small" @click="lookFile(scope.row)">预览</el-button>
        </template>
      </el-table-column>
    </el-table>
  </el-dialog>
  <filePreview ref="filePreviewRef" />
</template>
<script setup>
import { ref, getCurrentInstance } from 'vue'
import filePreview from '@/components/filePreview/index.vue'
const dialogVisible = ref(false)
const tableData = ref([])
const { proxy } = getCurrentInstance();
const filePreviewRef = ref()
const handleClose = () => {
  dialogVisible.value = false
}
const open = (list) => {
  dialogVisible.value = true
  tableData.value = list
}
const downLoadFile = (row) => {
  proxy.$download.name(row.url);
}
const lookFile = (row) => {
  filePreviewRef.value.open(row.url)
}
defineExpose({
  open
})
</script>
<style></style>
src/views/salesManagement/opportunityManagement/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,883 @@
<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 />
        <el-table-column label="商机来源" prop="businessSource" show-overflow-tooltip />
        <!-- <el-table-column label="客户描述" prop="description" show-overflow-tooltip min-width="200" /> -->
        <el-table-column label="录入人" prop="entryPerson" show-overflow-tooltip />
        <el-table-column label="更新日期" prop="updateTime">
          <template #default="{ row }">
            {{ formatDate(row.updateTime) }}
          </template>
        </el-table-column>
        <el-table-column label="操作" fixed="right" width="220" 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="handleAddOperation(row)"
            >
              æ·»åŠ æè¿°
            </el-button>
            <el-button
              link
              type="primary"
              size="small"
              @click="handleDetail(row)"
            >
              è¯¦æƒ…
            </el-button>
            <el-button
              link
              type="primary"
              size="small"
              @click="handleAttachment(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' ? '编辑商机' : operationType === 'addOperation' ? '添加商机' : '商机详情'"
      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' || operationType === 'addOperation'">
            <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' || operationType === 'addOperation'">
            <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' || operationType === 'addOperation'" />
        </el-form-item>
        <el-form-item label="客户描述" prop="description" v-if="operationType !== 'detail'">
          <el-input
            v-model="form.description"
            type="textarea"
            :rows="3"
            placeholder="请输入客户描述"
            maxlength="500"
            show-word-limit
          />
        </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' || operationType === 'addOperation'">
                <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="entryDate">
               <el-date-picker style="width: 100%" v-model="form.entryDate" value-format="YYYY-MM-DD" format="YYYY-MM-DD"
                 type="date" placeholder="请选择" clearable :disabled="operationType === 'detail' || operationType === 'addOperation'" />
             </el-form-item>
           </el-col>
        </el-row>
        <!-- é™„件上传(非详情模式下显示) -->
        <el-row :gutter="30" v-if="operationType !== 'detail'">
          <el-col :span="24">
            <el-form-item label="附件材料:">
              <el-upload v-model:file-list="fileList" :action="upload.url" multiple ref="fileUpload" auto-upload
                :headers="upload.headers" :data="upload.data" :before-upload="handleBeforeUpload" :on-error="handleUploadError"
                :on-success="handleUploadSuccess" :on-remove="handleRemove">
                <el-button type="primary">上传</el-button>
                <template #tip>
                  <div class="el-upload__tip">
                    æ–‡ä»¶æ ¼å¼æ”¯æŒ
                    doc,docx,xls,xlsx,ppt,pptx,pdf,txt,xml,jpg,jpeg,png,gif,bmp,rar,zip,7z
                  </div>
                </template>
              </el-upload>
            </el-form-item>
          </el-col>
        </el-row>
      </el-form>
      <!-- å˜æ›´è®°å½•时间线(仅在详情模式下显示) -->
      <div v-if="operationType === 'detail'" class="change-history-section">
        <el-divider content-position="left">变更记录</el-divider>
        <el-timeline>
          <el-timeline-item
            v-for="record in changeHistory"
            :key="record.id"
            :timestamp="record.timestamp"
            :type="record.type === 'current' ? 'primary' : record.type === 'update' ? 'success' : 'info'"
            :hollow="record.type === 'current'"
            placement="top"
          >
            <el-card shadow="hover" class="timeline-card">
              <template #header>
                <div class="card-header">
                  <span class="action-type">{{ record.action }}</span>
                  <span class="operator">操作人:{{ record.operator }}</span>
                </div>
              </template>
              <div class="change-content">
                <div class="status-change" v-if="record.status">
                  <span class="label">状态:</span>
                  <el-tag :type="record.type === 'current' ? 'primary' : 'info'" size="small">
                    {{ getStatusLabel(record.status) }}
                  </el-tag>
                </div>
                <div class="description-change" v-if="record.description">
                  <span class="label">客户描述:</span>
                  <span class="description-text">{{ record.description }}</span>
                </div>
              </div>
            </el-card>
          </el-timeline-item>
        </el-timeline>
      </div>
      <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>
    <!-- é™„件列表对话框 -->
    <FileList ref="fileListRef" />
  </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 { getToken } from '@/utils/auth'
import {
  opportunityListPage,
  addOpportunity,
  updateOpportunity,
  delOpportunity,
  addDescription
} from '@/api/salesManagement/opportunityManagement.js'
import { userListNoPage } from '@/api/system/user.js'
import {customerList, getSalesLedgerWithProducts} from '@/api/salesManagement/salesLedger.js'
import FileList from './fileList.vue'
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: undefined,
  province: '',
  customerName: '',
  businessSource: '',
  description: '',
  entryPerson: userStore.nickName,
  entryDate: dayjs().format('YYYY-MM-DD')
})
// å˜æ›´è®°å½•数据(模拟数据)
const changeHistory = ref([])
// æ–‡ä»¶åˆ—表
const fileList = ref([])
// FileList组件引用
const fileListRef = ref(null)
// ä¸Šä¼ é…ç½®
const upload = reactive({
  // ä¸Šä¼ çš„地址
  url: import.meta.env.VITE_APP_BASE_API + "/file/upload",
  // è®¾ç½®ä¸Šä¼ çš„请求头部
  headers: { Authorization: "Bearer " + getToken() },
  // ä¸Šä¼ å‚æ•°
  data: { type: 9 }
})
// èŽ·å–çŠ¶æ€æ ‡ç­¾
const getStatusLabel = (statusValue) => {
  const status = statusOptions.find(item => item.value === statusValue)
  return status ? status.label : statusValue
}
// è¡¨å•验证规则
const rules = reactive({
  customerName: [
    { required: true, message: '请选择客户', trigger: 'change' }
  ],
  status: [
    { required: true, message: '请选择状态', trigger: 'change' }
  ],
  entryPerson: [
    { required: true, message: '请选择录入人', trigger: 'change' }
  ],
  entryDate: [
    { required: true, message: '请选择录入日期', trigger: 'change' }
  ]
})
// çŠ¶æ€é€‰é¡¹
const statusOptions = [
  { value: '新建', label: '新建' },
  { value: '项目跟踪', label: '项目跟踪' },
  { value: '合同签约', label: '合同签约' },
  { value: '项目交付', label: '项目交付' },
  { value: '项目验收', label: '项目验收' }
]
// çœä»½é€‰é¡¹ï¼ˆç¤ºä¾‹ï¼‰
const provinceOptions = [
  { value: '北京市', label: '北京市' },
  { value: '天津市', label: '天津市' },
  { value: '河北省', label: '河北省' },
  { value: '山西省', label: '山西省' },
  { value: '内蒙古自治区', label: '内蒙古自治区' },
  { value: '辽宁省', label: '辽宁省' },
  { value: '吉林省', label: '吉林省' },
  { value: '黑龙江省', label: '黑龙江省' },
  { value: '上海市', label: '上海市' },
  { value: '江苏省', label: '江苏省' },
  { value: '浙江省', label: '浙江省' },
  { value: '安徽省', label: '安徽省' },
  { value: '福建省', label: '福建省' },
  { value: '江西省', label: '江西省' },
  { value: '山东省', label: '山东省' },
  { value: '河南省', label: '河南省' },
  { value: '湖北省', label: '湖北省' },
  { value: '湖南省', label: '湖南省' },
  { value: '广东省', label: '广东省' },
  { value: '广西壮族自治区', label: '广西壮族自治区' },
  { value: '海南省', label: '海南省' },
  { value: '重庆市', label: '重庆市' },
  { value: '四川省', label: '四川省' },
  { value: '贵州省', label: '贵州省' },
  { value: '云南省', label: '云南省' },
  { value: '西藏自治区', label: '西藏自治区' },
  { value: '陕西省', label: '陕西省' },
  { value: '甘肃省', label: '甘肃省' },
  { value: '青海省', label: '青海省' },
  { value: '宁夏回族自治区', label: '宁夏回族自治区' },
  { value: '新疆维吾尔自治区', label: '新疆维吾尔自治区' },
  { value: '台湾省', label: '台湾省' },
  { value: '香港特别行政区', label: '香港特别行政区' },
  { value: '澳门特别行政区', label: '澳门特别行政区' }
]
// èŽ·å–çŠ¶æ€æ ‡ç­¾ç±»åž‹
const getStatusTagType = (status) => {
  const typeMap = {
    '新建': 'info',
    '项目跟踪': 'primary',
    '合同签约': 'warning',
    '项目交付': 'success',
    '项目验收': 'success'
  }
  return typeMap[status] || 'info'
}
// èŽ·å–çŠ¶æ€æ–‡æœ¬
const getStatusText = (status) => {
  const textMap = {
    '新建': '新建',
    '项目跟踪': '项目跟踪',
    '合同签约': '合同签约',
    '项目交付': '项目交付',
    '项目验收': '项目验收'
  }
  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 handleAddOperation = async (row) => {
  operationType.value = 'addOperation'
  // åŠ è½½ç”¨æˆ·åˆ—è¡¨å’Œå®¢æˆ·åˆ—è¡¨
  let userLists = await userListNoPage()
  userList.value = userLists.data
  customerList().then((res) => {
    customerOption.value = res
  })
  // ä½¿ç”¨å½“前行数据作为基础,但只能修改状态和客户描述
  Object.assign(form, row, {
    // ä¿ç•™åŽŸå§‹å•†æœºID,用于关联操作记录
    status: row.status,
    description: '', // æ¸…空客户描述,允许重新填写
    entryPerson: userStore.nickName, // è®¾ç½®å½•入人为当前账号
    entryDate: dayjs().format('YYYY-MM-DD') // è®¾ç½®å½•入时间为当天
  })
  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
  })
  // ç”Ÿæˆæ¨¡æ‹Ÿå˜æ›´è®°å½•
  generateChangeHistory(row)
  dialogFormVisible.value = true
}
// ç”Ÿæˆå˜æ›´è®°å½•
const generateChangeHistory = (row) => {
  // ä½¿ç”¨businessDescription数组数据生成变更记录
  const history = []
  if (row.businessDescription && Array.isArray(row.businessDescription)) {
    row.businessDescription.forEach((item, index) => {
      history.push({
        id: item.id || index,
        timestamp: item.entryDate || item.updateTime || item.createTime,
        operator: item.entryPerson || '系统',
        status: item.status,
        description: item.description,
        type: index === 0 ? 'current' : 'info',
        action: index === 0 ? '当前状态' : '历史记录'
      })
    })
  }
  changeHistory.value = history
}
// ç¼–辑商机
const handleEdit = async (row) => {
  operationType.value = 'edit'
  // åŠ è½½ç”¨æˆ·åˆ—è¡¨å’Œå®¢æˆ·åˆ—è¡¨
  let userLists = await userListNoPage()
  userList.value = userLists.data
  customerList().then((res) => {
    customerOption.value = res
  })
  // ä½¿ç”¨å½“前账号和当天日期作为默认值
  Object.assign(form, row, {
    entryPerson: userStore.nickName, // è®¾ç½®å½•入人为当前账号
    entryDate: dayjs().format('YYYY-MM-DD') // è®¾ç½®å½•入时间为当天
  })
  dialogFormVisible.value = true
}
// å½•入人变化处理
const changs = (value) => {
  // å¯ä»¥æ ¹æ®éœ€è¦æ·»åŠ å¤„ç†é€»è¾‘
}
// æäº¤è¡¨å•
const submitForm = () => {
  formRef.value.validate(valid => {
    if (valid) {
      // æ”¶é›†é™„件文件的临时ID
      let tempFileIds = []
      if (fileList.value !== null && fileList.value.length > 0) {
        tempFileIds = fileList.value.map(item => item.tempId)
      }
      let api
      let successMessage
      let submitData
      if (operationType.value === 'add') {
        api = addOpportunity
        successMessage = '新建成功'
        submitData = {
          ...form,
          tempFileIds: tempFileIds,
          type: 9  // å•†æœºç®¡ç†çš„类型标识
        }
      } else if (operationType.value === 'addOperation') {
        api = addDescription
        successMessage = '添加操作成功'
        // æ·»åŠ æ“ä½œæ—¶ä¼ é€’çŠ¶æ€ã€æè¿°ã€å½•å…¥äººã€å½•å…¥æ—¥æœŸã€é™„ä»¶å’Œå•†æœºID
        submitData = {
          status: form.status,
          description: form.description,
          entryPerson: form.entryPerson,
          entryDate: form.entryDate,
          tempFileIds: tempFileIds,
          type: 9,  // å•†æœºç®¡ç†çš„类型标识
          businessOpportunityId: form.id  // ä¼ é€’商机ID
        }
      } else {
        api = updateOpportunity
        successMessage = '修改成功'
        submitData = {
          ...form,
          tempFileIds: tempFileIds,
          type: 9  // å•†æœºç®¡ç†çš„类型标识
        }
      }
      api(submitData).then(res => {
        if (res.code === 200) {
          proxy.$modal.msgSuccess(successMessage)
          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: '新建',
    province: '',
    customerName: '',
    businessSource: '',
    description: '',
    entryPerson: userStore.nickName,
    entryDate: dayjs().format('YYYY-MM-DD')
  })
  if (formRef.value) {
    formRef.value.clearValidate()
  }
}
// å…³é—­å¯¹è¯æ¡†
const closeDialog = () => {
  dialogFormVisible.value = false
  resetForm()
}
// ä¸Šä¼ å‰æ ¡æ£€
function handleBeforeUpload(file) {
  // æ ¡æ£€æ–‡ä»¶å¤§å°
  // if (file.size > 1024 * 1024 * 10) {
  //   proxy.$modal.msgError("上传文件大小不能超过10MB!");
  //   return false;
  // }
  proxy.$modal.loading("正在上传文件,请稍候...");
  return true;
}
// ä¸Šä¼ å¤±è´¥
function handleUploadError(err) {
  proxy.$modal.msgError("上传文件失败");
  proxy.$modal.closeLoading();
}
// ä¸Šä¼ æˆåŠŸå›žè°ƒ
function handleUploadSuccess(res, file, uploadFiles) {
  proxy.$modal.closeLoading();
  if (res.code === 200) {
    file.tempId = res.data.tempId;
    proxy.$modal.msgSuccess("上传成功");
  } else {
    proxy.$modal.msgError(res.msg);
    proxy.$refs.fileUpload.handleRemove(file);
  }
}
// ç§»é™¤æ–‡ä»¶
function handleRemove(file) {
  // è¿™é‡Œå¯ä»¥æ·»åŠ åˆ é™¤æ–‡ä»¶çš„é€»è¾‘
  proxy.$modal.msgSuccess("删除成功");
}
// æŸ¥çœ‹é™„ä»¶
function handleAttachment(row) {
    fileListRef.value.open(row.businessCommonFiles)
}
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;
    }
  }
}
/* å˜æ›´è®°å½•时间线样式 */
.change-history-section {
  margin-top: 20px;
  .el-divider {
    margin: 20px 0;
  }
  .timeline-card {
    margin: 8px 0;
    border-radius: 8px;
    .card-header {
      display: flex;
      justify-content: space-between;
      align-items: center;
      padding: 8px 0;
      .action-type {
        font-weight: 600;
        color: #333;
      }
      .operator {
        font-size: 12px;
        color: #666;
      }
    }
    .change-content {
      .status-change, .description-change {
        margin-bottom: 8px;
        .label {
          font-weight: 500;
          color: #666;
          margin-right: 8px;
        }
        .description-text {
          color: #333;
          line-height: 1.5;
        }
      }
    }
  }
  /* æ—¶é—´çº¿æ ·å¼ä¼˜åŒ– */
  :deep(.el-timeline) {
    padding-left: 0;
    .el-timeline-item {
      .el-timeline-item__node {
        background-color: #409eff;
        &.el-timeline-item__node--primary {
          background-color: #409eff;
        }
        &.el-timeline-item__node--success {
          background-color: #67c23a;
        }
        &.el-timeline-item__node--info {
          background-color: #909399;
        }
        &.el-timeline-item__node--hollow {
          background-color: transparent;
          border-color: #409eff;
        }
      }
      .el-timeline-item__timestamp {
        color: #666;
        font-size: 12px;
      }
    }
  }
}
</style>
vite.config.js
@@ -8,7 +8,7 @@
  const { VITE_APP_ENV } = env;
  const baseUrl =
    VITE_APP_ENV == "development"
      ? "http://114.132.189.42:7003" // å¼€å‘环境后端接口
      ? "http://192.168.1.147:7003" // å¼€å‘环境后端接口
      : "http://114.132.189.42:7003"; // ç”Ÿäº§çŽ¯å¢ƒåŽç«¯æŽ¥å£
  return {