| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022 |
- <template>
- <el-drawer
- :title="detailData.projectName"
- v-model="visible"
- direction="rtl"
- size="85%"
- :close-on-click-modal="false"
- class="detail-drawer"
- >
- <div class="detail-container">
- <!-- 顶部进度条区域 -->
- <div class="top-progress">
- <div class="section-title">项目进度</div>
- <div class="chevron-steps">
- <div
- v-for="(step, index) in steps"
- :key="index"
- class="chevron-step"
- :class="{
- 'is-active': currentStepIndex === index,
- 'is-completed': currentStepIndex > index,
- }"
- @click="handleStepClick(index)"
- >
- <span class="step-text">{{ step }}</span>
- </div>
- </div>
- </div>
- <!-- 下部分栏区域 -->
- <div class="main-content">
- <!-- 左侧面板 -->
- <div class="left-panel">
- <el-tabs v-model="leftActiveTab" class="custom-tabs">
- <el-tab-pane label="项目信息" name="info">
- <div class="info-block">
- <div class="info-header">
- <span class="title">基本信息</span>
- <el-button type="primary" class="edit-btn" @click="$emit('edit', detailData)" size="default">
- <el-icon style="margin-right: 4px;"><Edit /></el-icon>编辑
- </el-button>
- </div>
- <el-descriptions :column="3" class="custom-desc">
- <el-descriptions-item label="项目名称:">{{ detailData.projectName }}</el-descriptions-item>
- <el-descriptions-item label=" "></el-descriptions-item>
- <el-descriptions-item label=" "></el-descriptions-item>
-
- <el-descriptions-item label="归属公司:">{{ detailData.companyName }}</el-descriptions-item>
- <el-descriptions-item label="客户名称:" :span="2">{{ detailData.customerName }}</el-descriptions-item>
-
- <el-descriptions-item label="行业:">{{ findIndustryName(detailData.industry) }}</el-descriptions-item>
- <el-descriptions-item label="部门:">{{ detailData.deptName }}</el-descriptions-item>
- <el-descriptions-item label="金额(万):">{{ detailData.projectBudget != null ? Number(detailData.projectBudget).toFixed(2) : '' }}</el-descriptions-item>
-
- <el-descriptions-item label="赢单率(%):">{{ detailData.winRate }}</el-descriptions-item>
- <el-descriptions-item label="立项时间:">{{ proxy.parseTime(detailData.approvalDate, '{y}-{m}-{d}') }}</el-descriptions-item>
- <el-descriptions-item label="截止时间:">{{ proxy.parseTime(detailData.expectedCompletionTime, '{y}-{m}-{d}') }}</el-descriptions-item>
-
- <el-descriptions-item label="项目状态:">{{ getSaleStatusLabel(detailData.status) }}</el-descriptions-item>
- <el-descriptions-item label="营销活动:" :span="2">{{ detailData.marketingActivityName || detailData.activityNo }}</el-descriptions-item>
- </el-descriptions>
- </div>
- <div class="info-block" style="margin-top: 30px;">
- <div class="info-header">
- <span class="title">项目情况</span>
- </div>
- <el-descriptions :column="3" class="custom-desc">
- <el-descriptions-item label="商机来源:">{{ findInfoSourceName(detailData.infoSource) }}</el-descriptions-item>
- <el-descriptions-item label="项目级别:">{{ findProjectLevelName(detailData.projectLevel) }}</el-descriptions-item>
- <el-descriptions-item label="项目区域:">{{ detailData.projectArea }}</el-descriptions-item>
-
- <el-descriptions-item label="采购方式:">{{ findProcurementMethodName(detailData.procurementMethod) }}</el-descriptions-item>
-
- <el-descriptions-item label="项目描述:" :span="3">{{ detailData.projectDescription }}</el-descriptions-item>
- <el-descriptions-item label="竞争对手:" :span="3">{{ detailData.competitor }}</el-descriptions-item>
- </el-descriptions>
- </div>
- </el-tab-pane>
- <el-tab-pane label="项目联系人" name="contact">
- <div class="tab-toolbar">
- <el-dropdown @command="handleContactCommand">
- <el-button type="primary">
- <el-icon style="margin-right: 4px;"><Plus /></el-icon>新建联系人<el-icon class="el-icon--right"><arrow-down /></el-icon>
- </el-button>
- <template #dropdown>
- <el-dropdown-menu>
- <el-dropdown-item command="associate">关联客户联系人</el-dropdown-item>
- <el-dropdown-item command="new">新建联系人</el-dropdown-item>
- </el-dropdown-menu>
- </template>
- </el-dropdown>
- </div>
- <el-table :data="contactList" border class="contact-table" style="width: 100%" v-loading="contactLoading">
- <el-table-column label="姓名" align="center" width="100">
- <template #default="scope">
- <el-link type="primary" :underline="false" @click="handleEditContact(scope.row)">
- {{ scope.row.contactName || scope.row.name }}
- </el-link>
- </template>
- </el-table-column>
- <el-table-column label="性别" align="center" width="80">
- <template #default="scope">
- {{ scope.row.gender === '0' || scope.row.sex === '0' ? '男' : scope.row.gender === '1' || scope.row.sex === '1' ? '女' : '' }}
- </template>
- </el-table-column>
- <el-table-column label="部门" align="center">
- <template #default="scope">
- {{ scope.row.deptName || scope.row.department }}
- </template>
- </el-table-column>
- <el-table-column label="职位" align="center">
- <template #default="scope">
- {{ scope.row.position || scope.row.roleName || scope.row.role }}
- </template>
- </el-table-column>
- <el-table-column label="项目角色" align="center">
- <template #default="scope">
- {{ scope.row.projectRole || scope.row.role }}
- </template>
- </el-table-column>
- <el-table-column label="是否关键人" align="center" width="100">
- <template #default="scope">
- <span>{{ (scope.row.isKeyPerson === 1 || scope.row.keyPerson === 1) ? '是' : '否' }}</span>
- </template>
- </el-table-column>
- <el-table-column label="操作" align="center" width="120" fixed="right">
- <template #default="scope">
- <el-button link type="primary" size="small" @click="handleEditContact(scope.row)">编辑</el-button>
- <el-button link type="danger" size="small" @click="handleDeleteContact(scope.row)">删除</el-button>
- </template>
- </el-table-column>
- <template #empty><span class="empty-text">暂无数据</span></template>
- </el-table>
- </el-tab-pane>
- <el-tab-pane label="结果分析" name="analysis">
- <div class="analysis-section">
- <div class="analysis-form">
- <div class="analysis-row">
- <span class="form-label">成交结果</span>
- <el-radio-group v-model="analysisForm.resultType" class="result-radio">
- <el-radio value="win">赢单</el-radio>
- <el-radio value="lose">丢单</el-radio>
- </el-radio-group>
- <div style="flex:1"></div>
- <el-button type="primary" icon="CircleCheck" @click="handleSaveAnalysis">保 存</el-button>
- </div>
- <template v-if="analysisForm.resultType === 'win'">
- <div class="summary-title">赢单总结</div>
- <div class="summary-textarea-wrap">
- <el-input v-model="analysisForm.summary" type="textarea" :rows="4" placeholder="请输入赢单总结" />
- </div>
- </template>
- <template v-else>
- <div class="summary-title">丢单原因</div>
- <div class="summary-textarea-wrap">
- <el-input v-model="analysisForm.loseReason" type="textarea" :rows="4" placeholder="请输入丢单原因" />
- </div>
- </template>
- <div class="attachment-section-title">附件</div>
- <div class="upload-area">
- <el-upload
- :action="uploadFileUrl"
- :headers="headers"
- :on-success="handleAnalysisUploadSuccess"
- :show-file-list="false"
- multiple
- >
- <el-button type="primary" icon="Upload">上 传</el-button>
- </el-upload>
- </div>
- <div v-if="analysisFileList.length > 0" class="analysis-file-list" style="margin-top:10px;">
- <el-table :data="analysisFileList" border size="small">
- <el-table-column label="文件名称" prop="name" show-overflow-tooltip />
- <el-table-column label="操作" width="120" align="center">
- <template #default="scope">
- <el-button link type="primary" @click="downloadFile(scope.row)">下载</el-button>
- <el-button link type="danger" @click="handleAnalysisDeleteFile(scope.$index)">删除</el-button>
- </template>
- </el-table-column>
- </el-table>
- </div>
- </div>
- </div>
- </el-tab-pane>
- <el-tab-pane label="附件" name="attachment">
- <div class="tab-toolbar">
- <el-upload
- :action="uploadFileUrl"
- :headers="headers"
- :on-success="handleUploadSuccess"
- :show-file-list="false"
- multiple
- >
- <el-button type="primary" icon="Upload">上传附件</el-button>
- </el-upload>
- </div>
- <el-table :data="detailFileList" border class="custom-table" style="width: 100%">
- <el-table-column label="文件名称" align="center" prop="name" show-overflow-tooltip />
- <el-table-column label="文件类型" align="center" prop="type" width="100" />
- <el-table-column label="上传时间" align="center" prop="uploadTime" width="160" />
- <el-table-column label="操作" align="center" width="120">
- <template #default="scope">
- <el-link :href="scope.row.url" :underline="false" target="_blank" type="primary" style="margin-right: 10px;">下载</el-link>
- <el-link type="danger" :underline="false" @click="handleDeleteFile(scope.row)">删除</el-link>
- </template>
- </el-table-column>
- <template #empty><span class="empty-text">暂无数据</span></template>
- </el-table>
- </el-tab-pane>
- </el-tabs>
- </div>
- <!-- 右侧面板:通用业务活动组件 -->
- <div class="right-panel">
- <BusinessActivity
- v-if="detailData.id"
- :businessId="detailData.id"
- :infoData="detailData"
- businessType="opportunity"
- @success="getList"
- />
- </div>
- </div>
- </div>
- </el-drawer>
- <!-- 关联客户联系人弹窗 -->
- <el-dialog title="关联客户联系人" v-model="associateVisible" width="900px" append-to-body>
- <el-table :data="associateList" v-loading="associateLoading" border @selection-change="handleSelectionChange" class="custom-table" max-height="400px">
- <el-table-column type="selection" width="55" />
- <el-table-column label="联系人" align="center" prop="contactName" width="120" />
- <el-table-column label="部门" align="center" prop="deptName" width="130" />
- <el-table-column label="客户名称" align="center" prop="customerName" min-width="180" show-overflow-tooltip />
- <el-table-column label="职位" align="center" prop="roleName" width="110" />
- <el-table-column label="手机号码" align="center" prop="phone" width="120" />
- <el-table-column label="办公电话" align="center" prop="officePhone" width="120" />
- </el-table>
- <template #footer>
- <div class="dialog-footer" style="padding-top: 10px;">
- <el-button type="primary" @click="confirmAssociate" :disabled="selectedContacts.length === 0">确 认</el-button>
- <el-button @click="associateVisible = false">取 消</el-button>
- </div>
- </template>
- </el-dialog>
- <!-- 编辑/新建项目联系人抽屉 (完全对齐原型图) -->
- <el-drawer v-model="projectContactDrawerOpen" direction="rtl" size="80%" destroy-on-close :with-header="false">
- <div class="drawer-header-standard" style="padding: 15px 20px; border-bottom: 1px solid #f0f0f0; display: flex; justify-content: space-between; align-items: center;">
- <span style="font-size: 16px; font-weight: normal; color: #333;">{{ projectContactForm.id ? '编辑项目联系人' : '新建项目联系人' }}</span>
- <el-icon @click="projectContactDrawerOpen = false" style="cursor: pointer; font-size: 20px;"><Close /></el-icon>
- </div>
- <div class="drawer-body-custom" style="padding: 0; overflow-y: auto; height: calc(100% - 110px);">
- <el-form :model="projectContactForm" :rules="projectContactRules" ref="projectContactFormRef" label-width="100px" class="no-bold-label">
- <!-- 基本信息 -->
- <div class="form-section-group">
- <div class="section-group-title">基本信息</div>
- <div class="section-group-content">
- <el-row :gutter="20">
- <el-col :span="8">
- <el-form-item label="姓名" prop="name">
- <el-input v-model="projectContactForm.name" placeholder="请输入" />
- </el-form-item>
- </el-col>
- <el-col :span="8">
- <el-form-item label="联系人类型" prop="contactType">
- <el-radio-group v-model="projectContactForm.contactType">
- <el-radio value="1">公司职员</el-radio>
- <el-radio value="2">关系资源人</el-radio>
- </el-radio-group>
- </el-form-item>
- </el-col>
- <el-col :span="8">
- <el-form-item label="性别" prop="sex">
- <el-radio-group v-model="projectContactForm.sex">
- <el-radio value="0">男</el-radio>
- <el-radio value="1">女</el-radio>
- </el-radio-group>
- </el-form-item>
- </el-col>
- </el-row>
- <el-row :gutter="20">
- <el-col :span="8">
- <el-form-item label="年龄" prop="age">
- <el-input v-model="projectContactForm.age" placeholder="请输入" />
- </el-form-item>
- </el-col>
- <el-col :span="8">
- <el-form-item label="籍贯" prop="nativePlace">
- <el-input v-model="projectContactForm.nativePlace" placeholder="请输入" />
- </el-form-item>
- </el-col>
- <el-col :span="8">
- <el-form-item label="生日" prop="birthday">
- <el-date-picker v-model="projectContactForm.birthday" type="date" placeholder="请选择" style="width: 100%" value-format="YYYY-MM-DD" />
- </el-form-item>
- </el-col>
- </el-row>
- <el-row :gutter="20">
- <el-col :span="24">
- <el-form-item label="描述" prop="remark">
- <el-input v-model="projectContactForm.remark" type="textarea" :rows="2" placeholder="请输入" />
- </el-form-item>
- </el-col>
- </el-row>
- </div>
- </div>
- <!-- 办公信息 -->
- <div class="form-section-group">
- <div class="section-group-title">办公信息</div>
- <div class="section-group-content">
- <el-row :gutter="20">
- <el-col :span="8">
- <el-form-item label="在职状态" prop="status">
- <el-radio-group v-model="projectContactForm.status">
- <el-radio value="0">在职</el-radio>
- <el-radio value="1">离职</el-radio>
- </el-radio-group>
- </el-form-item>
- </el-col>
- <el-col :span="8">
- <el-form-item label="手机号码" prop="phonenumber">
- <el-input v-model="projectContactForm.phonenumber" placeholder="请输入" />
- </el-form-item>
- </el-col>
- <el-col :span="8">
- <el-form-item label="部门" prop="deptName">
- <el-input v-model="projectContactForm.deptName" placeholder="请输入" />
- </el-form-item>
- </el-col>
- </el-row>
- <el-row :gutter="20">
- <el-col :span="8">
- <el-form-item label="职位" prop="position">
- <el-input v-model="projectContactForm.position" placeholder="请输入" />
- </el-form-item>
- </el-col>
- <el-col :span="8">
- <el-form-item label="办公座机" prop="officePhone">
- <el-input v-model="projectContactForm.officePhone" placeholder="请输入" />
- </el-form-item>
- </el-col>
- </el-row>
- <el-row :gutter="20">
- <el-col :span="24">
- <el-form-item label="办公地址" prop="address">
- <div style="display: flex; gap: 10px;">
- <el-input v-model="projectContactForm.provincialCityCounty" placeholder="请选择" style="width: 200px" readonly />
- <el-input v-model="projectContactForm.addressDetail" placeholder="请输入详细地址" />
- </div>
- </el-form-item>
- </el-col>
- </el-row>
- <el-row :gutter="20">
- <el-col :span="24">
- <el-form-item label="工作内容" prop="jobContent">
- <el-input v-model="projectContactForm.jobContent" type="textarea" :rows="2" placeholder="请输入" />
- </el-form-item>
- </el-col>
- </el-row>
- </div>
- </div>
- <!-- 项目决策 -->
- <div class="form-section-group">
- <div class="section-group-title">项目决策</div>
- <div class="section-group-content">
- <el-row :gutter="20">
- <el-col :span="8">
- <el-form-item label="项目角色" prop="projectRole">
- <el-select v-model="projectContactForm.projectRole" style="width: 100%" placeholder="请选择" filterable>
- <el-option v-for="item in projectRoleOptions" :key="item.value" :label="item.label" :value="item.value" />
- </el-select>
- </el-form-item>
- </el-col>
- <el-col :span="8">
- <el-form-item label="是否关键人" prop="isKeyPerson">
- <el-select v-model="projectContactForm.isKeyPerson" style="width: 100%" placeholder="请选择">
- <el-option label="是" :value="1" />
- <el-option label="否" :value="0" />
- </el-select>
- </el-form-item>
- </el-col>
- </el-row>
- <el-row :gutter="20">
- <el-col :span="24">
- <el-form-item label="公关情况" prop="prStatus">
- <el-input v-model="projectContactForm.prStatus" type="textarea" :rows="2" placeholder="请输入" />
- </el-form-item>
- </el-col>
- </el-row>
- </div>
- </div>
- </el-form>
- </div>
- <div class="drawer-footer-standard" style="padding: 15px 20px; border-top: 1px solid #f0f0f0; text-align: right;">
- <el-button @click="projectContactDrawerOpen = false">取 消</el-button>
- <el-button type="primary" @click="submitProjectContact" :loading="contactSubmitting">确 定</el-button>
- </div>
- </el-drawer>
- </template>
- <script setup>
- import { ref, computed, reactive, watch, getCurrentInstance, toRefs } from 'vue';
- import { Plus, Search, Close, Upload, Edit, ArrowDown, CircleCheck } from '@element-plus/icons-vue';
- import { getOpportunity, updateOpportunity } from '@/api/saleManage/opportunity/index';
- import { listContact, addContact, delContact, updateContact } from "@/api/customer/crmContact";
- import { getContactPerson } from "@/api/customer/contactPerson";
- import { getSalesResultAnalyzeByObjectNo, addSalesResultAnalyze, updateSalesResultAnalyze } from '@/api/saleManage/leads/salesResultAnalyze';
- import { listByIds } from "@/api/system/oss/index";
- import { listIndustryCategory } from "@/api/customer/industryCategory";
- import { globalHeaders } from '@/utils/request';
- import { ElMessageBox, ElMessage } from 'element-plus';
- import BusinessActivity from '@/views/common/businessActivity.vue';
- const props = defineProps({
- modelValue: Boolean,
- id: [String, Number],
- saleStatusOptions: { type: Array, default: () => [] },
- projectLevelOptions: { type: Array, default: () => [] },
- infoSourceOptions: { type: Array, default: () => [] },
- procurementMethodOptions: { type: Array, default: () => [] }
- });
- const emit = defineEmits(['update:modelValue', 'edit', 'success']);
- const steps = ['标前磋商', '立项公示', '应标投标', '结案'];
- const currentStepIndex = computed(() => {
- const schedule = detailData.value.projectSchedule;
- if (!schedule || schedule === '0') return -1;
- return parseInt(schedule) - 1;
- });
- const visible = ref(false);
- const detailData = ref({});
- const leftActiveTab = ref('info');
- const industryOptions = ref([]);
- const proxy = getCurrentInstance().proxy;
- // 状态同步
- watch(() => props.modelValue, (val) => { visible.value = val; });
- watch(() => visible.value, (val) => { emit('update:modelValue', val); });
- // 附件管理相关
- const detailFileList = ref([]);
- const uploadFileUrl = ref(import.meta.env.VITE_APP_BASE_API + '/resource/oss/upload');
- const headers = ref(globalHeaders());
- // 项目联系人
- const contactList = ref([]);
- const contactLoading = ref(false);
- const contactSubmitting = ref(false);
- const projectContactDrawerOpen = ref(false);
- const projectContactFormRef = ref(null);
- const projectContactForm = reactive({
- id: undefined,
- name: '',
- contactType: '1',
- sex: '0',
- age: '',
- nativePlace: '',
- birthday: '',
- remark: '',
- status: '0',
- phonenumber: '',
- deptName: '',
- position: '',
- officePhone: '',
- provincialCityCounty: '',
- addressDetail: '',
- jobContent: '',
- projectRole: '',
- isKeyPerson: 0,
- prStatus: ''
- });
- const projectContactRules = {
- name: [{ required: true, message: '姓名不能为空', trigger: 'blur' }],
- phonenumber: [{ required: true, message: '手机号码不能为空', trigger: 'blur' }],
- };
- // 结果分析相关
- const analysisForm = reactive({
- id: undefined,
- resultType: 'win',
- summary: '',
- loseReason: '',
- fileNo: ''
- });
- const analysisFileList = ref([]);
- // 关联联系人相关
- const associateVisible = ref(false);
- const associateLoading = ref(false);
- const associateList = ref([]);
- const selectedContacts = ref([]);
- const associateQuery = reactive({ contactName: '', phone: '' });
- const { LXRJE0001: projectRoleOptions } = toRefs(reactive(proxy.useDict("LXRJE0001")));
- const getFileType = (name) => {
- if (!name) return '未知';
- const parts = name.split('.');
- return parts.length > 1 ? parts[parts.length - 1].toUpperCase() : '文件';
- };
- watch(() => detailData.value.fileNo, async (val) => {
- if (val) {
- try {
- const res = await listByIds(val);
- detailFileList.value = (res.data || []).map(item => ({
- name: item.originalName,
- url: item.url,
- ossId: item.ossId,
- type: getFileType(item.originalName),
- uploadTime: item.createTime
- }));
- } catch (e) {}
- } else {
- detailFileList.value = [];
- }
- }, { immediate: true });
- const open = async (row, tab) => {
- visible.value = true;
- leftActiveTab.value = 'info';
- detailData.value = { ...row };
-
- // 预加载字典
- loadDicts();
-
- try {
- const id = row.id;
- if (id) {
- const res = await getOpportunity(id);
- if (res.data) {
- Object.assign(detailData.value, res.data);
- }
- fetchContactList(id);
- getIndustryList();
- loadAnalysisData();
- }
- } catch (error) {}
- };
- const loadAnalysisData = async () => {
- if (!detailData.value.projectNo) return;
- try {
- const res = await getSalesResultAnalyzeByObjectNo(detailData.value.projectNo);
- if (res.data) {
- Object.assign(analysisForm, {
- id: res.data.id,
- resultType: res.data.dealResult === 1 ? 'win' : 'lose',
- summary: res.data.winSumUp,
- loseReason: res.data.loseReason,
- fileNo: res.data.fileNo
- });
- if (analysisForm.fileNo) {
- const ossRes = await listByIds(analysisForm.fileNo);
- analysisFileList.value = ossRes.data.map(i => ({
- name: i.originalName || i.fileName,
- url: i.url,
- ossId: i.ossId,
- type: (i.originalName || i.fileName || '').split('.').pop().toUpperCase()
- }));
- }
- } else {
- // 重置表单
- Object.assign(analysisForm, {
- id: undefined,
- resultType: 'win',
- summary: '',
- loseReason: '',
- fileNo: ''
- });
- analysisFileList.value = [];
- }
- } catch (e) {}
- };
- const handleAnalysisUploadSuccess = (res) => {
- if (res.code === 200) {
- const file = res.data;
- const fileName = file.originalName || file.fileName || '未知文件';
- analysisFileList.value.push({
- name: fileName,
- url: file.url,
- ossId: file.ossId,
- type: fileName.split('.').pop().toUpperCase()
- });
- proxy.$modal.msgSuccess("上传成功");
- } else {
- proxy.$modal.msgError(res.msg || "上传失败");
- }
- };
- const handleAnalysisDeleteFile = (index) => {
- analysisFileList.value.splice(index, 1);
- };
- const handleSaveAnalysis = async () => {
- if (!detailData.value.projectNo) {
- proxy.$modal.msgWarning("项目编号缺失,无法保存");
- return;
- }
-
- const ossIds = analysisFileList.value.map(f => f.ossId).join(',');
- const payload = {
- id: analysisForm.id,
- objectNo: detailData.value.projectNo,
- dealResult: analysisForm.resultType === 'win' ? 1 : 2,
- winSumUp: analysisForm.summary,
- loseReason: analysisForm.loseReason,
- fileNo: ossIds
- };
- proxy.$modal.loading("正在保存...");
- try {
- if (analysisForm.id) {
- await updateSalesResultAnalyze(payload);
- } else {
- await addSalesResultAnalyze(payload);
- }
- proxy.$modal.msgSuccess("保存成功");
- loadAnalysisData();
- } catch (e) {
- } finally {
- proxy.$modal.closeLoading();
- }
- };
- const downloadFile = (file) => {
- if (file.url) {
- window.open(file.url, '_blank');
- }
- };
- const loadDicts = () => {
- };
- const getIndustryList = async () => {
- try {
- const res = await listIndustryCategory();
- industryOptions.value = res.data || [];
- } catch (error) {}
- };
- const fetchContactList = async (id) => {
- if (!id) return;
- contactLoading.value = true;
- try {
- const res = await listContact({ platformCode: String(id) });
- contactList.value = res.rows || [];
- } catch (e) {} finally {
- contactLoading.value = false;
- }
- };
- const handleContactCommand = (command) => {
- if (command === 'associate') {
- handleAssociate();
- } else if (command === 'new') {
- handleNewProjectContact();
- }
- };
- const handleAssociate = () => {
- associateQuery.contactName = '';
- associateQuery.phone = '';
- associateVisible.value = true;
- loadAssociateList();
- };
- const loadAssociateList = async () => {
- if (!detailData.value.companyName) {
- proxy.$modal.msgWarning("当前商机未绑定客户,无法关联联系人");
- return;
- }
- associateVisible.value = true;
- associateLoading.value = true;
- // 根据客户名称查询联系人 (注意:后端参数名为 customName,对齐线索模块逻辑)
- listContact({ customName: detailData.value.companyName }).then(res => {
- // 过滤掉已经关联到当前项目的联系人
- const currentIds = contactList.value.map(c => c.id);
- associateList.value = (res.rows || []).filter(item => !currentIds.includes(item.id));
- }).finally(() => {
- associateLoading.value = false;
- });
- };
- const handleSelectionChange = (selection) => {
- selectedContacts.value = selection;
- };
- const confirmAssociate = async () => {
- if (selectedContacts.value.length === 0) return;
- proxy.$modal.loading("正在关联...");
- try {
- const promises = selectedContacts.value.map(contact => {
- return updateContact({
- ...contact,
- platformCode: String(detailData.value.id)
- });
- });
- await Promise.all(promises);
- ElMessage.success('关联成功');
- associateVisible.value = false;
- fetchContactList(detailData.value.id);
- } catch (e) {} finally {
- proxy.$modal.closeLoading();
- }
- };
- const handleNewProjectContact = () => {
- Object.assign(projectContactForm, {
- id: undefined, name: '', contactType: '1', sex: '0', age: '', deptName: '',
- position: '', projectRole: '', isKeyPerson: 0, status: '1', phonenumber: ''
- });
- projectContactDrawerOpen.value = true;
- };
- const handleEditContact = (row) => {
- Object.assign(projectContactForm, {
- id: row.id,
- name: row.contactName || row.name,
- sex: row.gender || row.sex || '0',
- deptName: row.deptName || row.department,
- position: row.position || row.roleName || row.role,
- projectRole: row.projectRole || row.role,
- isKeyPerson: row.isKeyPerson || (row.keyPerson ? 1 : 0),
- phonenumber: row.phonenumber || row.phone,
- contactType: row.contactType || '1'
- });
- projectContactDrawerOpen.value = true;
- };
- const submitProjectContact = () => {
- projectContactFormRef.value.validate(async (valid) => {
- if (valid) {
- contactSubmitting.value = true;
- try {
- const payload = {
- ...projectContactForm,
- platformCode: String(detailData.value.id),
- customerNo: detailData.value.companyNo
- };
- if (projectContactForm.id) {
- await updateContact(payload);
- } else {
- await addContact(payload);
- }
- ElMessage.success('保存成功');
- projectContactDrawerOpen.value = false;
- fetchContactList(detailData.value.id);
- } catch (e) {} finally {
- contactSubmitting.value = false;
- }
- }
- });
- };
- const handleDeleteContact = (row) => {
- ElMessageBox.confirm(`确认删除联系人「${row.name}」吗?`, '提示', { type: 'warning' }).then(async () => {
- await delContact(row.id);
- ElMessage.success('操作成功');
- fetchContactList(detailData.value.id);
- });
- };
- const handleUploadSuccess = async (res) => {
- if (res.code === 200) {
- const ossId = res.data.ossId;
- const currentFileNos = detailData.value.fileNo ? detailData.value.fileNo.split(',') : [];
- currentFileNos.push(ossId);
- const newFileNo = currentFileNos.join(',');
-
- try {
- await updateOpportunity({ id: detailData.value.id, fileNo: newFileNo });
- detailData.value.fileNo = newFileNo;
- ElMessage.success('上传成功');
- } catch (e) {}
- }
- };
- const handleDeleteFile = (row) => {
- ElMessageBox.confirm(`确认删除附件「${row.name}」吗?`, '提示', { type: 'warning' }).then(async () => {
- const currentFileNos = detailData.value.fileNo ? detailData.value.fileNo.split(',') : [];
- const newFileNo = currentFileNos.filter(id => String(id) !== String(row.ossId)).join(',');
-
- try {
- proxy.$modal.loading("正在删除...");
- await updateOpportunity({ id: detailData.value.id, fileNo: newFileNo });
- detailData.value.fileNo = newFileNo;
- ElMessage.success('删除成功');
- } catch (e) {
- } finally {
- proxy.$modal.closeLoading();
- }
- });
- };
- const handleStepClick = async (index) => {
- const targetSchedule = String(index + 1);
- const isCancel = targetSchedule === String(detailData.value.projectSchedule);
-
- try {
- const finalSchedule = isCancel ? String(index) : targetSchedule;
- await updateOpportunity({
- id: detailData.value.id,
- projectSchedule: finalSchedule,
- status: finalSchedule === '4' ? '1' : detailData.value.status
- });
- detailData.value.projectSchedule = finalSchedule;
- if (finalSchedule === '4') detailData.value.status = '1';
- ElMessage.success('操作成功');
- emit('success');
- } catch (error) {
- console.error('更新进度失败:', error);
- }
- };
- const getSaleStatusLabel = (status) => {
- if (String(status) === '0') return '跟进中';
- if (String(status) === '1') return '结案';
- return props.saleStatusOptions.find(i => String(i.value) === String(status))?.label || status;
- };
- const findProjectLevelName = (level) => {
- return props.projectLevelOptions.find(i => String(i.value) === String(level))?.label || level;
- };
- const findInfoSourceName = (source) => {
- return props.infoSourceOptions.find(i => String(i.value) === String(source))?.label || source;
- };
- const findProcurementMethodName = (method) => {
- return props.procurementMethodOptions.find(i => String(i.value) === String(method))?.label || method;
- };
- const findIndustryName = (val) => {
- return industryOptions.value.find(i => String(i.id) === String(val))?.industryCategoryName || val;
- };
- const getList = () => {
- emit('success');
- };
- defineExpose({ open });
- </script>
- <style lang="scss" scoped>
- .detail-drawer {
- :deep(.el-drawer__header) {
- margin: 0;
- padding: 0 20px;
- height: 48px;
- line-height: 48px;
- border-bottom: 1px solid #f0f0f0;
- span {
- font-size: 16px;
- font-weight: normal;
- color: #333;
- }
- }
- :deep(.el-drawer__body) { padding: 0 !important; background-color: #fff; }
- }
- .detail-container { height: 100%; display: flex; flex-direction: column; background: #fff; }
- .top-progress {
- padding: 5px 20px 12px; background-color: #fff; border-bottom: 1px solid #f5f5f5;
- .section-title { font-size: 14px; font-weight: normal !important; color: #333; margin-bottom: 12px; }
- .chevron-steps {
- display: flex; align-items: center; gap: 0; padding: 2px 0;
- .chevron-step {
- flex: 1; height: 32px; display: flex; align-items: center; justify-content: center; background-color: #F2F3F5; color: #86909C; font-size: 13px; position: relative; cursor: pointer; transition: all 0.3s;
- margin-right: -10px; // 负边距实现重叠
- clip-path: polygon(calc(100% - 10px) 0%, 100% 50%, calc(100% - 10px) 100%, 0% 100%, 10px 50%, 0% 0%);
-
- &:first-child {
- border-radius: 4px 0 0 4px;
- clip-path: polygon(calc(100% - 10px) 0%, 100% 50%, calc(100% - 10px) 100%, 0% 100%, 0% 0%);
- }
- &:last-child {
- border-radius: 0 4px 4px 0;
- margin-right: 0;
- clip-path: polygon(100% 0%, 100% 100%, 0% 100%, 10px 50%, 0% 0%);
- }
-
- &.is-active { background-color: #00B881; color: #fff; z-index: 2; font-weight: 500 !important; }
- &.is-completed { background-color: #E6F8F3; color: #00B881; z-index: 1; }
- &:hover:not(.is-active) { background-color: #E5E6EB; z-index: 3; }
- }
- }
- }
- .main-content {
- display: flex; flex: 1; overflow: hidden;
- .left-panel { flex: 7; background: #fff; border-right: 1px solid #f0f0f0; padding: 5px 20px 15px; overflow-y: auto; }
- .right-panel { flex: 3; background: #fff; overflow-y: auto; }
- }
- .info-block {
- .info-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; .title { font-size: 15px; font-weight: normal !important; color: #409eff; padding-left: 0; } }
- }
- .custom-tabs {
- :deep(.el-tabs__header) { margin-bottom: 15px; height: 56px; border-bottom: 1px solid #f0f0f0; }
- :deep(.el-tabs__nav-wrap::after) { height: 0; }
- :deep(.el-tabs__item) { font-size: 14px; font-weight: normal !important; height: 56px; line-height: 56px; }
- }
- .tab-toolbar { margin-bottom: 10px; display: flex; justify-content: flex-end; }
- .custom-desc {
- :deep(.el-descriptions__label) { color: #86909C; font-weight: normal !important; min-width: 80px; padding-bottom: 8px !important; }
- :deep(.el-descriptions__content) { color: #1D2129; padding-bottom: 8px !important; }
- }
- .section-block { margin-bottom: 15px; .section-title { font-size: 14px; font-weight: normal !important; color: #333; margin-bottom: 10px; padding-bottom: 6px; border-bottom: 1px solid #f0f0f0; } }
- .form-section-group {
- margin-bottom: 20px;
- .section-group-title {
- padding: 8px 15px;
- background-color: #f8fbff;
- color: #409eff;
- font-size: 14px;
- margin-bottom: 15px;
- }
- .section-group-content { padding: 0 15px; }
- }
- .no-bold-label {
- :deep(.el-form-item__label) { font-weight: normal !important; color: #666; }
- }
- .contact-table {
- :deep(th.el-table__cell) {
- background-color: #f8f9fb !important;
- color: #666;
- font-weight: 500;
- }
- }
- .custom-table {
- :deep(th.el-table__cell) { font-weight: normal !important; background-color: #f8f9fa !important; }
- :deep(.el-table__cell) { font-weight: normal !important; }
- }
- :deep(.el-dialog) {
- .el-dialog__body { padding: 10px 20px 20px; }
- .el-table { border-radius: 0; }
- }
- /* 结果分析样式 */
- .analysis-section {
- padding: 15px 0;
- .analysis-form {
- .analysis-row {
- display: flex;
- align-items: center;
- margin-bottom: 20px;
- .form-label {
- font-size: 14px;
- color: #333;
- margin-right: 15px;
- font-weight: 500;
- }
- .result-radio {
- margin-right: 20px;
- :deep(.el-radio) {
- margin-right: 15px;
- .el-radio__label {
- font-weight: normal;
- }
- }
- }
- }
- .summary-title {
- font-size: 14px;
- color: #333;
- margin-bottom: 10px;
- font-weight: 500;
- }
- .summary-textarea-wrap {
- margin-bottom: 25px;
- :deep(.el-textarea__inner) {
- border-radius: 4px;
- background-color: #fff;
- border-color: #e5e6eb;
- padding: 10px 12px;
- font-size: 13px;
- &:focus {
- background-color: #fff;
- border-color: #409eff;
- }
- }
- }
- .attachment-section-title {
- font-size: 14px;
- color: #333;
- margin-bottom: 12px;
- font-weight: 500;
- }
- .upload-area {
- margin-bottom: 15px;
- }
- }
- }
- .drawer-body-custom {
- &::-webkit-scrollbar {
- display: none;
- }
- -ms-overflow-style: none;
- scrollbar-width: none;
- }
- </style>
|