| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068 |
- <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="营销活动:">{{ detailData.marketingActivityName || detailData.activityNo }}</el-descriptions-item>
- <el-descriptions-item label=" "></el-descriptions-item>
- <el-descriptions-item label="项目负责人:">{{ detailData.leaderName || findUserName(detailData.leader) }}</el-descriptions-item>
- <el-descriptions-item label="产品支持:">{{ detailData.productSupportName || findUserName(detailData.productSupport) }}</el-descriptions-item>
- <el-descriptions-item label=" "></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="采购方式:" :span="3">{{ findProcurementMethodName(detailData.procurementMethod) }}</el-descriptions-item>
-
- <el-descriptions-item label="项目描述:" :span="3">
- <div class="text-content">{{ detailData.projectDescription }}</div>
- </el-descriptions-item>
- <el-descriptions-item label="竞争对手:" :span="3">
- <div class="text-content">{{ detailData.competitor }}</div>
- </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-button link type="primary" @click="downloadFile(scope.row)" style="margin-right: 10px;">下载</el-button>
- <el-button link type="danger" @click="handleDeleteFile(scope.row)">删除</el-button>
- </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,
- industryName: findIndustryName(detailData.industry),
- profession: findIndustryName(detailData.industry)
- }"
- 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 { useDebounceFn } from '@vueuse/core';
- 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: () => [] },
- userOptions: { 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;
- const findUserName = (id) => {
- if (!id) return '';
- const user = props.userOptions.find(u => String(u.staffId || u.userId) === String(id));
- return user ? (user.staffName || user.nickName) : id;
- };
- // 状态同步
- 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 = useDebounceFn(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();
- }
- }, 300);
- const downloadFile = (file) => {
- if (file.url) {
- proxy.$modal.msgInfo("正在启动下载...");
- if (proxy.$download && proxy.$download.resource) {
- proxy.$download.resource(file.url);
- } else {
- 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 = useDebounceFn(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();
- }
- }, 300);
- 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 = useDebounceFn(() => {
- projectContactFormRef.value.validate(async (valid) => {
- if (valid) {
- contactSubmitting.value = true;
- try {
- const payload = {
- ...projectContactForm,
- platformCode: String(detailData.value.id),
- // 关联客户信息
- customerName: detailData.value.customerName || '',
- customerNo: detailData.value.companyNo || '',
- customNo: 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;
- }
- }
- });
- }, 300);
- const handleDeleteContact = useDebounceFn((row) => {
- ElMessageBox.confirm(`确认删除联系人「${row.name}」吗?`, '提示', { type: 'warning' }).then(async () => {
- await delContact(row.id);
- ElMessage.success('操作成功');
- fetchContactList(detailData.value.id);
- });
- }, 300);
- 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 = useDebounceFn((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();
- }
- });
- }, 300);
- 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) => {
- if (!val) return '';
- // 处理可能的多个 ID(以逗号分隔)
- const ids = String(val).split(',').filter(Boolean);
- if (ids.length > 1) {
- return ids.map(id => {
- return industryOptions.value.find(i => String(i.id) === String(id))?.industryCategoryName || id;
- }).join('、');
- }
- 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;
- }
- .text-content {
- white-space: pre-wrap;
- word-break: break-all;
- line-height: 1.6;
- color: #1D2129;
- }
- </style>
|