detail.vue 55 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253
  1. <template>
  2. <el-drawer
  3. v-model="visible"
  4. direction="rtl"
  5. size="80%"
  6. class="platform-detail-drawer"
  7. :destroy-on-close="true"
  8. :with-header="false"
  9. >
  10. <!-- 自定义头部 -->
  11. <div class="detail-header">
  12. <div class="header-left">
  13. <span class="project-title">{{ drawerForm.projectName }}</span>
  14. </div>
  15. <div class="header-right">
  16. <el-icon class="close-btn" @click="visible = false"><Close /></el-icon>
  17. </div>
  18. </div>
  19. <!-- 详情内容区 -->
  20. <div class="detail-main-scroll" v-loading="loading">
  21. <!-- 项目进度 (扁平化设计) -->
  22. <div class="project-progress-card">
  23. <div class="section-title">项目进度</div>
  24. <div class="chevron-steps">
  25. <div v-for="(step, idx) in stepList" :key="idx" class="chevron-step"
  26. :class="{
  27. 'is-finished': (drawerForm.projectStatus || 1) > (idx + 1),
  28. 'is-active': (drawerForm.projectStatus || 1) === (idx + 1)
  29. }"
  30. @click="handleStepClick(idx + 1)">
  31. <span class="step-text">{{ step }}</span>
  32. </div>
  33. </div>
  34. <div class="progress-footer">
  35. <span class="sub-label">最新进度</span>
  36. <span class="all-progress-link">全部进度</span>
  37. </div>
  38. </div>
  39. <div class="detail-content-layout">
  40. <!-- 左侧核心信息 -->
  41. <div class="content-left-panel">
  42. <div class="detail-content-card">
  43. <el-tabs v-model="drawerActiveTab" class="custom-tabs sticky-tabs">
  44. <el-tab-pane label="项目信息" name="info">
  45. <!-- 基本信息 -->
  46. <div class="info-section">
  47. <div class="info-section-header">
  48. <span class="title">基本信息</span>
  49. <el-button type="primary" size="small" @click="handleEdit">
  50. <el-icon style="margin-right: 4px;"><Edit /></el-icon>编辑
  51. </el-button>
  52. </div>
  53. <el-descriptions :column="3" class="custom-desc">
  54. <el-descriptions-item label="项目名称" :span="3">{{ drawerForm.projectName }}</el-descriptions-item>
  55. <el-descriptions-item label="归属公司">{{ displayCompanyName }}</el-descriptions-item>
  56. <el-descriptions-item label="客户名称">{{ drawerForm.customName || drawerForm.customerName }}</el-descriptions-item>
  57. <el-descriptions-item label="行业">{{ displayIndustryName }}</el-descriptions-item>
  58. <el-descriptions-item label="部门">{{ displayDeptName }}</el-descriptions-item>
  59. <el-descriptions-item label="项目状态">{{ getStatusLabel(drawerForm.projectStatus) }}</el-descriptions-item>
  60. <el-descriptions-item label="项目级别">{{ displayProjectLevelName }}</el-descriptions-item>
  61. <el-descriptions-item label="项目类型">{{ displayProjectTypeName }}</el-descriptions-item>
  62. <el-descriptions-item label="平台名称" :span="2">{{ drawerForm.platformName }}</el-descriptions-item>
  63. <el-descriptions-item label="平台链接" :span="3">{{ drawerForm.platformLink }}</el-descriptions-item>
  64. </el-descriptions>
  65. </div>
  66. <!-- 项目情况 -->
  67. <div class="info-section mt-24">
  68. <div class="info-section-header">
  69. <span class="title">项目情况</span>
  70. </div>
  71. <el-descriptions :column="3" class="custom-desc">
  72. <el-descriptions-item label="登记日期">{{ formatDate(drawerForm.createTime) }}</el-descriptions-item>
  73. <el-descriptions-item label="金额(万)">{{ drawerForm.amount ? Number(drawerForm.amount).toFixed(2) : '' }}</el-descriptions-item>
  74. <el-descriptions-item label="报名费">{{ drawerForm.entryFee ? Number(drawerForm.entryFee).toFixed(2) : '' }}</el-descriptions-item>
  75. <el-descriptions-item label="投标保证金">{{ drawerForm.bidBond ? Number(drawerForm.bidBond).toFixed(2) : '' }}</el-descriptions-item>
  76. <el-descriptions-item label="赢单率(%)">{{ drawerForm.winningRate ? drawerForm.winningRate + '%' : (drawerForm.winRate ? drawerForm.winRate + '%' : '') }}</el-descriptions-item>
  77. <el-descriptions-item label="报名截止时间">{{ formatDate(drawerForm.signUpDeadline) }}</el-descriptions-item>
  78. <el-descriptions-item label="投标截止时间">{{ formatDate(drawerForm.tenderDeadline) }}</el-descriptions-item>
  79. <el-descriptions-item label="标书汇编完成时间">{{ formatDate(drawerForm.docCompileTime) }}</el-descriptions-item>
  80. <el-descriptions-item label="服务期(年)">{{ drawerForm.standardPeriod }}</el-descriptions-item>
  81. <el-descriptions-item label="服务时间段" :span="1">{{ drawerForm.serviceTime }}</el-descriptions-item>
  82. <el-descriptions-item label="入围类型" :span="2">{{ displayShortlistedName }}</el-descriptions-item>
  83. <el-descriptions-item label="物资类目" :span="3">{{ displayProfessionName }}</el-descriptions-item>
  84. <el-descriptions-item label="招标代理机构" :span="1">{{ drawerForm.biddingAgency }}</el-descriptions-item>
  85. <el-descriptions-item label="机构联系方式" :span="2">{{ drawerForm.agencyContact }}</el-descriptions-item>
  86. <el-descriptions-item label="标期类型">{{ drawerForm.bidPeriodType == 2 ? '周期性框架' : '单项目入围' }}</el-descriptions-item>
  87. <template v-if="drawerForm.bidPeriodType == 2">
  88. <el-descriptions-item label="预计下次投标时间">{{ formatDate(drawerForm.nextBiddingTime) }}</el-descriptions-item>
  89. <el-descriptions-item label="提前提醒天数">{{ drawerForm.noticeAdvanceDays }}</el-descriptions-item>
  90. </template>
  91. <template v-else>
  92. <el-descriptions-item :span="2" label-class-name="hidden-label" class-name="hidden-value"></el-descriptions-item>
  93. </template>
  94. <el-descriptions-item label="入围要求" :span="3">{{ drawerForm.shortlistedRequirement }}</el-descriptions-item>
  95. <el-descriptions-item label="项目描述" :span="3">{{ drawerForm.projectDesc }}</el-descriptions-item>
  96. </el-descriptions>
  97. </div>
  98. </el-tab-pane>
  99. <el-tab-pane label="项目联系人" name="contact">
  100. <div class="contact-tab-content">
  101. <div class="tab-toolbar">
  102. <el-dropdown @command="handleContactCommand">
  103. <el-button type="primary">
  104. <el-icon style="margin-right: 4px;"><Plus /></el-icon>新建联系人<el-icon class="el-icon--right"><arrow-down /></el-icon>
  105. </el-button>
  106. <template #dropdown>
  107. <el-dropdown-menu>
  108. <el-dropdown-item command="associate">关联客户联系人</el-dropdown-item>
  109. <el-dropdown-item command="new">新建联系人</el-dropdown-item>
  110. </el-dropdown-menu>
  111. </template>
  112. </el-dropdown>
  113. </div>
  114. <el-table :data="contactList" v-loading="contactLoading" class="custom-table" border>
  115. <el-table-column label="姓名" align="center" width="100">
  116. <template #default="scope">
  117. <el-link type="primary" :underline="false" @click="handleEditContact(scope.row)">
  118. {{ scope.row.contactName || scope.row.name }}
  119. </el-link>
  120. </template>
  121. </el-table-column>
  122. <el-table-column label="性别" align="center" width="80">
  123. <template #default="scope">
  124. {{ (scope.row.gender == '0' || scope.row.sex == '0') ? '男' : (scope.row.gender == '1' || scope.row.sex == '1' ? '女' : '') }}
  125. </template>
  126. </el-table-column>
  127. <el-table-column label="部门" prop="deptName" align="center" show-overflow-tooltip />
  128. <el-table-column label="职位" align="center" show-overflow-tooltip>
  129. <template #default="scope">
  130. {{ scope.row.position || scope.row.roleName || scope.row.role }}
  131. </template>
  132. </el-table-column>
  133. <el-table-column label="项目角色" align="center">
  134. <template #default="scope">
  135. {{ getProjectRoleLabel(scope.row.projectRole) }}
  136. </template>
  137. </el-table-column>
  138. <el-table-column label="是否关键人" align="center" width="100">
  139. <template #default="scope">
  140. <span>{{ (scope.row.isKeyPerson === 1 || scope.row.keyPerson === 1) ? '是' : '否' }}</span>
  141. </template>
  142. </el-table-column>
  143. <el-table-column label="操作" align="center" width="120" fixed="right">
  144. <template #default="scope">
  145. <el-button link type="primary" @click="handleEditContact(scope.row)">编辑</el-button>
  146. <el-button link type="danger" @click="handleDeleteContact(scope.row)">删除</el-button>
  147. </template>
  148. </el-table-column>
  149. </el-table>
  150. </div>
  151. </el-tab-pane>
  152. <el-tab-pane label="结果分析" name="analysis">
  153. <div class="analysis-tab-content">
  154. <div class="analysis-form">
  155. <div class="analysis-row">
  156. <span class="form-label">成交结果</span>
  157. <el-radio-group v-model="analysisForm.resultType" class="result-radio">
  158. <el-radio value="win">赢单</el-radio>
  159. <el-radio value="lose">丢单</el-radio>
  160. </el-radio-group>
  161. <div style="flex:1"></div>
  162. <el-button type="primary" icon="CircleCheck" @click="handleSaveAnalysis">保 存</el-button>
  163. </div>
  164. <template v-if="analysisForm.resultType === 'win'">
  165. <div class="summary-title">赢单总结</div>
  166. <div class="summary-textarea-wrap">
  167. <el-input v-model="analysisForm.summary" type="textarea" :rows="4" placeholder="请输入赢单总结" />
  168. </div>
  169. </template>
  170. <template v-else>
  171. <div class="summary-title">丢单原因</div>
  172. <div class="summary-textarea-wrap">
  173. <el-input v-model="analysisForm.loseReason" type="textarea" :rows="4" placeholder="请输入丢单原因" />
  174. </div>
  175. </template>
  176. <div class="attachment-section-title">附件</div>
  177. <div class="upload-area">
  178. <el-upload
  179. :action="uploadFileUrl"
  180. :headers="headers"
  181. :on-success="handleAnalysisUploadSuccess"
  182. :show-file-list="false"
  183. multiple
  184. >
  185. <el-button type="primary" icon="Upload">上 传</el-button>
  186. </el-upload>
  187. </div>
  188. <div v-if="analysisFileList.length > 0" class="analysis-file-list" style="margin-top:10px;">
  189. <el-table :data="analysisFileList" border size="small">
  190. <el-table-column label="文件名称" prop="name" align="center" show-overflow-tooltip />
  191. <el-table-column label="操作" width="120" align="center">
  192. <template #default="scope">
  193. <el-button link type="primary" @click="downloadFile(scope.row)">下载</el-button>
  194. <el-button link type="danger" @click="handleAnalysisDeleteFile(scope.$index)">删除</el-button>
  195. </template>
  196. </el-table-column>
  197. </el-table>
  198. </div>
  199. </div>
  200. </div>
  201. </el-tab-pane>
  202. <el-tab-pane label="附件" name="files">
  203. <div class="files-tab-content">
  204. <div class="tab-toolbar">
  205. <el-upload
  206. :action="uploadFileUrl"
  207. :headers="headers"
  208. :on-success="handleUploadSuccess"
  209. :show-file-list="false"
  210. multiple
  211. >
  212. <el-button type="primary" icon="Upload">上传附件</el-button>
  213. </el-upload>
  214. </div>
  215. <el-table :data="detailFileList" border class="custom-table">
  216. <el-table-column label="文件名称" prop="name" show-overflow-tooltip />
  217. <el-table-column label="上传时间" prop="uploadTime" width="180" align="center">
  218. <template #default="scope"><span>{{ formatDate(scope.row.uploadTime) }}</span></template>
  219. </el-table-column>
  220. <el-table-column label="操作" width="120" align="center">
  221. <template #default="scope">
  222. <el-button link type="primary" @click="downloadFile(scope.row)">下载</el-button>
  223. <el-button link type="danger" @click="handleDeleteFile(scope.row)">删除</el-button>
  224. </template>
  225. </el-table-column>
  226. </el-table>
  227. </div>
  228. </el-tab-pane>
  229. </el-tabs>
  230. </div>
  231. </div>
  232. <!-- 右侧跟进活动 -->
  233. <div class="content-right-panel">
  234. <business-activity
  235. :business-id="props.id"
  236. business-type="platformSelection"
  237. :info-data="{
  238. ...drawerForm,
  239. customerName: drawerForm.customName || drawerForm.customerName,
  240. deptName: displayDeptName,
  241. department: displayDeptName,
  242. profession: displayIndustryName,
  243. industryName: displayIndustryName,
  244. /* 显式传入负责人姓名,兼容多种字段名,避免回退到系统创建人 */
  245. leaderName: drawerForm.leaderName || drawerForm.managerName || drawerForm.staffName || ''
  246. }"
  247. />
  248. </div>
  249. </div>
  250. </div>
  251. </el-drawer>
  252. <!-- 项目联系人抽屉 -->
  253. <el-drawer
  254. v-model="projectContactDrawerOpen"
  255. direction="rtl"
  256. size="80%"
  257. destroy-on-close
  258. :with-header="false"
  259. >
  260. <div class="drawer-header-compact" style="height: 40px; padding: 0 16px; border-bottom: 1px solid #e2e8f0; display: flex; justify-content: space-between; align-items: center; background: #fff;">
  261. <span style="font-size: 15px; font-weight: 600; color: #333;">{{ projectContactForm.id ? '编辑项目联系人' : '新建项目联系人' }}</span>
  262. <el-icon @click="projectContactDrawerOpen = false" style="cursor: pointer; font-size: 18px; color: #94a3b8;"><Close /></el-icon>
  263. </div>
  264. <div class="drawer-body-custom" style="padding: 24px; overflow-y: auto; height: calc(100% - 120px);">
  265. <el-form :model="projectContactForm" :rules="projectContactRules" ref="projectContactFormRef" label-width="100px" label-position="right" class="no-bold-label">
  266. <!-- 基本信息 -->
  267. <div class="form-section-group">
  268. <div class="section-group-title-bar">基本信息</div>
  269. <el-row :gutter="20">
  270. <el-col :span="8">
  271. <el-form-item label="姓名" prop="contactName"><el-input v-model="projectContactForm.contactName" placeholder="请输入" /></el-form-item>
  272. </el-col>
  273. <el-col :span="8">
  274. <el-form-item label="联系人类型" prop="type">
  275. <el-radio-group v-model="projectContactForm.type">
  276. <el-radio value="1">公司职员</el-radio>
  277. <el-radio value="2">关系资源人</el-radio>
  278. </el-radio-group>
  279. </el-form-item>
  280. </el-col>
  281. <el-col :span="8">
  282. <el-form-item label="性别" prop="gender">
  283. <el-radio-group v-model="projectContactForm.gender">
  284. <el-radio value="0">男</el-radio><el-radio value="1">女</el-radio>
  285. </el-radio-group>
  286. </el-form-item>
  287. </el-col>
  288. </el-row>
  289. <el-row :gutter="20">
  290. <el-col :span="8">
  291. <el-form-item label="年龄" prop="age"><el-input v-model="projectContactForm.age" placeholder="请输入" /></el-form-item>
  292. </el-col>
  293. <el-col :span="8">
  294. <el-form-item label="籍贯" prop="nativePlace"><el-input v-model="projectContactForm.nativePlace" placeholder="请输入" /></el-form-item>
  295. </el-col>
  296. <el-col :span="8">
  297. <el-form-item label="生日" prop="birthday">
  298. <el-date-picker v-model="projectContactForm.birthday" type="date" placeholder="请选择" style="width: 100%" value-format="YYYY-MM-DD" />
  299. </el-form-item>
  300. </el-col>
  301. </el-row>
  302. <el-row :gutter="20">
  303. <el-col :span="24">
  304. <el-form-item label="描述" prop="remark"><el-input v-model="projectContactForm.remark" type="textarea" :rows="3" placeholder="请输入" /></el-form-item>
  305. </el-col>
  306. </el-row>
  307. </div>
  308. <!-- 办公信息 -->
  309. <div class="form-section-group">
  310. <div class="section-group-title-bar">办公信息</div>
  311. <el-row :gutter="20">
  312. <el-col :span="8">
  313. <el-form-item label="在职状态" prop="jobStatus">
  314. <el-radio-group v-model="projectContactForm.jobStatus">
  315. <el-radio value="1">在职</el-radio>
  316. <el-radio value="2">离职</el-radio>
  317. </el-radio-group>
  318. </el-form-item>
  319. </el-col>
  320. <el-col :span="8">
  321. <el-form-item label="手机号码" prop="phone"><el-input v-model="projectContactForm.phone" placeholder="请输入" /></el-form-item>
  322. </el-col>
  323. <el-col :span="8">
  324. <el-form-item label="部门" prop="deptName"><el-input v-model="projectContactForm.deptName" placeholder="请输入" /></el-form-item>
  325. </el-col>
  326. </el-row>
  327. <el-row :gutter="20">
  328. <el-col :span="8">
  329. <el-form-item label="职位" prop="position"><el-input v-model="projectContactForm.position" placeholder="请输入" /></el-form-item>
  330. </el-col>
  331. <el-col :span="8">
  332. <el-form-item label="办公座机" prop="officePhone"><el-input v-model="projectContactForm.officePhone" placeholder="请输入" /></el-form-item>
  333. </el-col>
  334. </el-row>
  335. <el-row :gutter="20">
  336. <el-col :span="24">
  337. <el-form-item label="办公地址" prop="addressDetail">
  338. <div style="display: flex; gap: 10px;">
  339. <el-cascader
  340. v-model="projectContactForm.officeRegion"
  341. :options="areaOptions"
  342. :props="{ label: 'areaName', value: 'id', children: 'children' }"
  343. placeholder="请选择省/市/区"
  344. style="width: 250px"
  345. clearable
  346. />
  347. <el-input v-model="projectContactForm.addressDetail" placeholder="请输入详细地址" style="flex: 1" />
  348. </div>
  349. </el-form-item>
  350. </el-col>
  351. </el-row>
  352. <el-row :gutter="20">
  353. <el-col :span="24">
  354. <el-form-item label="工作内容" prop="jobContent"><el-input v-model="projectContactForm.jobContent" type="textarea" :rows="3" placeholder="请输入" /></el-form-item>
  355. </el-col>
  356. </el-row>
  357. </div>
  358. <!-- 项目决策 -->
  359. <div class="form-section-group">
  360. <div class="section-group-title-bar">项目决策</div>
  361. <el-row :gutter="20">
  362. <el-col :span="8">
  363. <el-form-item label="项目角色" prop="projectRole">
  364. <el-select v-model="projectContactForm.projectRole" style="width: 100%" placeholder="请选择" filterable clearable>
  365. <el-option v-for="item in projectRoleOptions" :key="item.value" :label="item.label" :value="parseInt(item.value)" />
  366. </el-select>
  367. </el-form-item>
  368. </el-col>
  369. <el-col :span="8">
  370. <el-form-item label="是否关键人" prop="isKeyPerson">
  371. <el-select v-model="projectContactForm.isKeyPerson" style="width: 100%" placeholder="请选择">
  372. <el-option label="是" :value="1" /><el-option label="否" :value="0" />
  373. </el-select>
  374. </el-form-item>
  375. </el-col>
  376. </el-row>
  377. <el-row :gutter="20">
  378. <el-col :span="24">
  379. <el-form-item label="公关情况" prop="prStatus"><el-input v-model="projectContactForm.prStatus" type="textarea" :rows="3" placeholder="请输入" /></el-form-item>
  380. </el-col>
  381. </el-row>
  382. </div>
  383. <!-- 家庭信息 -->
  384. <div class="form-section-group">
  385. <div class="section-group-title-bar">家庭信息</div>
  386. <el-row :gutter="20">
  387. <el-col :span="24">
  388. <el-form-item label="家庭住址" prop="homeAddressDetail">
  389. <div style="display: flex; gap: 10px;">
  390. <el-cascader
  391. v-model="projectContactForm.homeRegion"
  392. :options="areaOptions"
  393. :props="{ label: 'areaName', value: 'id', children: 'children' }"
  394. placeholder="请选择省/市/区"
  395. style="width: 250px"
  396. clearable
  397. />
  398. <el-input v-model="projectContactForm.homeAddressDetail" placeholder="请输入详细地址" style="flex: 1" />
  399. </div>
  400. </el-form-item>
  401. </el-col>
  402. </el-row>
  403. <el-row :gutter="20">
  404. <el-col :span="8">
  405. <el-form-item label="家庭情况" prop="familyStatus"><el-input v-model="projectContactForm.familyStatus" placeholder="请输入" /></el-form-item>
  406. </el-col>
  407. <el-col :span="8">
  408. <el-form-item label="爱好" prop="hobby"><el-input v-model="projectContactForm.hobby" placeholder="请输入" /></el-form-item>
  409. </el-col>
  410. <el-col :span="8">
  411. <el-form-item label="性格特征" prop="characterTrait"><el-input v-model="projectContactForm.characterTrait" placeholder="请输入" /></el-form-item>
  412. </el-col>
  413. </el-row>
  414. <el-row :gutter="20">
  415. <el-col :span="8">
  416. <el-form-item label="是否抽烟" prop="isSmoke">
  417. <el-radio-group v-model="projectContactForm.isSmoke">
  418. <el-radio value="1">是</el-radio><el-radio value="0">否</el-radio>
  419. </el-radio-group>
  420. </el-form-item>
  421. </el-col>
  422. <el-col :span="8">
  423. <el-form-item label="是否喝酒" prop="isDrink">
  424. <el-radio-group v-model="projectContactForm.isDrink">
  425. <el-radio value="1">是</el-radio><el-radio value="0">否</el-radio>
  426. </el-radio-group>
  427. </el-form-item>
  428. </el-col>
  429. </el-row>
  430. </div>
  431. </el-form>
  432. </div>
  433. <div class="drawer-footer-standard" style="padding: 16px 24px; border-top: 1px solid #f0f0f0; text-align: right;">
  434. <el-button @click="projectContactDrawerOpen = false">取 消</el-button>
  435. <el-button type="primary" @click="submitProjectContact" :loading="contactSubmitting">确 定</el-button>
  436. </div>
  437. </el-drawer>
  438. <!-- 关联联系人对话框 -->
  439. <el-dialog title="关联客户联系人" v-model="associateVisible" width="900px" append-to-body>
  440. <el-table :data="associateList" v-loading="associateLoading" border @selection-change="handleSelectionChange" max-height="400px">
  441. <el-table-column type="selection" width="55" />
  442. <el-table-column label="姓名" align="center" prop="contactName" width="120" />
  443. <el-table-column label="部门" align="center" prop="deptName" show-overflow-tooltip />
  444. <el-table-column label="职位" align="center" prop="roleName" width="150" />
  445. <el-table-column label="手机号码" align="center" prop="phone" width="150" />
  446. </el-table>
  447. <template #footer>
  448. <div class="dialog-footer">
  449. <el-button @click="associateVisible = false">取 消</el-button>
  450. <el-button type="primary" @click="confirmAssociate" :disabled="selectedContacts.length === 0">确 认</el-button>
  451. </div>
  452. </template>
  453. </el-dialog>
  454. </template>
  455. <script setup>
  456. import { ref, reactive, computed, watch, getCurrentInstance, toRefs } from 'vue';
  457. import { getPlatformSelection, updatePlatformSelection } from '@/api/saleManage/platformSelection/index';
  458. import { listContact, addContact, delContact, updateContact, getContact } from '@/api/customer/crmContact';
  459. import { getSalesResultAnalyzeByObjectNo, addSalesResultAnalyze, updateSalesResultAnalyze } from '@/api/saleManage/leads/salesResultAnalyze';
  460. import { listByIds } from "@/api/system/oss/index";
  461. import { globalHeaders } from '@/utils/request';
  462. import { listProvinceWithCities } from "@/api/customer/addressArea";
  463. import { useDebounceFn } from '@vueuse/core';
  464. import BusinessActivity from '@/views/common/businessActivity.vue';
  465. import { ElMessageBox, ElMessage } from 'element-plus';
  466. import { listComStaff } from '@/api/system/comStaff/index';
  467. import { listCustomerInfo } from "@/api/customer/customerInfo/index";
  468. const proxy = getCurrentInstance().proxy;
  469. const props = defineProps({
  470. modelValue: Boolean,
  471. id: [String, Number],
  472. options: { type: Object, default: () => ({}) }
  473. });
  474. const emit = defineEmits(['update:modelValue', 'edit', 'success']);
  475. const visible = computed({
  476. get: () => props.modelValue,
  477. set: (val) => emit('update:modelValue', val)
  478. });
  479. const loading = ref(false);
  480. const drawerForm = ref({ projectStatus: 1, fileList: [], memberList: [] });
  481. const drawerActiveTab = ref('info');
  482. const stepList = ['获取信息', '正式立项', '竞价投标', '项目跟进', '结案'];
  483. /**
  484. * loadData 后调用:当后端详情接口未返回 leaderName / deptName 文字时,
  485. * 通过 listComStaff(staffId) 查询 CRM 员工信息(含 staffName 和 deptName)进行补全。
  486. */
  487. const enrichFormNames = async () => {
  488. const data = drawerForm.value;
  489. const leaderId = data.leader || data.managerId;
  490. // leaderName 和 deptName 都已有文字,不需要查询
  491. const hasLeaderName = data.leaderName && isNaN(Number(data.leaderName));
  492. const hasDeptName = data.deptName && isNaN(Number(data.deptName));
  493. if (hasLeaderName && hasDeptName) return;
  494. if (!leaderId) return;
  495. try {
  496. const res = await listComStaff({ staffId: leaderId });
  497. // listComStaff 返回分页结构,取第一条匹配的员工
  498. const staff = (res.rows || res.data || [])[0];
  499. if (!staff) return;
  500. drawerForm.value = {
  501. ...drawerForm.value,
  502. // 只在当前字段为空时覆盖
  503. leaderName: hasLeaderName ? data.leaderName : (staff.staffName || ''),
  504. deptName: hasDeptName ? data.deptName : (staff.deptName || '')
  505. };
  506. } catch (e) {
  507. // 静默失败,不影响主流程
  508. }
  509. };
  510. const contactList = ref([]);
  511. const contactLoading = ref(false);
  512. const contactSubmitting = ref(false);
  513. const projectContactDrawerOpen = ref(false);
  514. const projectContactFormRef = ref(null);
  515. const projectContactForm = reactive({
  516. id: undefined,
  517. contactName: '',
  518. type: '1',
  519. gender: '0',
  520. age: '',
  521. nativePlace: '',
  522. birthday: '',
  523. remark: '',
  524. jobStatus: '1',
  525. phone: '',
  526. deptName: '',
  527. position: '',
  528. officePhone: '',
  529. officeRegion: [],
  530. addressDetail: '',
  531. jobContent: '',
  532. projectRole: null,
  533. isKeyPerson: 0,
  534. prStatus: '',
  535. familyStatus: '',
  536. hobby: '',
  537. characterTrait: '',
  538. isSmoke: '0',
  539. isDrink: '0',
  540. homeRegion: [],
  541. homeAddressDetail: ''
  542. });
  543. const projectContactRules = {
  544. contactName: [{ required: true, message: '姓名不能为空', trigger: 'blur' }],
  545. age: [{ required: true, message: '年龄不能为空', trigger: 'blur' }],
  546. deptName: [{ required: true, message: '部门不能为空', trigger: 'blur' }],
  547. phone: [
  548. { pattern: /^1[3-9]\d{9}$/, message: '请输入正确的11位手机号码', trigger: 'blur' }
  549. ],
  550. officePhone: [
  551. { pattern: /^1[3-9]\d{9}$/, message: '座机号请输入正确的11位手机号码', trigger: 'blur' }
  552. ]
  553. };
  554. const { LXRJE0001: projectRoleOptions } = toRefs(reactive(proxy.useDict("LXRJE0001")));
  555. const areaOptions = ref([]);
  556. const associateVisible = ref(false);
  557. const associateLoading = ref(false);
  558. const associateList = ref([]);
  559. const selectedContacts = ref([]);
  560. // 结果分析逻辑
  561. const analysisForm = reactive({ id: undefined, resultType: 'win', summary: '', loseReason: '', fileNo: '' });
  562. const analysisFileList = ref([]);
  563. // 附件管理
  564. const detailFileList = ref([]);
  565. const uploadFileUrl = ref(import.meta.env.VITE_APP_BASE_API + '/resource/oss/upload');
  566. const headers = ref(globalHeaders());
  567. watch(() => props.modelValue, (val) => {
  568. if (val && props.id) {
  569. loadData();
  570. }
  571. });
  572. const loadData = async () => {
  573. loading.value = true;
  574. try {
  575. const res = await getPlatformSelection(props.id);
  576. const data = res.data || {};
  577. drawerForm.value = {
  578. ...data,
  579. // 字段兼容映射
  580. materialCategory: data.materialCategory || data.profession,
  581. biddingAgency: data.biddingAgency || data.agency,
  582. nextBiddingTime: data.nextBiddingTime || data.nextBidTime,
  583. noticeAdvanceDays: data.noticeAdvanceDays ?? data.reminderDays,
  584. serviceTime: data.serviceTime || data.serviceTimeRange,
  585. signUpDeadline: data.signUpDeadline || data.entryDeadline,
  586. tenderDeadline: data.tenderDeadline || data.bidDeadline,
  587. shortlistedType: data.shortlistedType || data.finalizationType,
  588. shortlistedRequirement: data.shortlistedRequirement || data.condition || data.requirement,
  589. industry: data.industry || data.industryName || data.professionName,
  590. deptName: data.deptName || data.createDeptName || data.dept?.deptName || data.createDept
  591. };
  592. // 通过客户编号查询真实的客户 ID
  593. const cNo = drawerForm.value.customNo || drawerForm.value.companyNo || drawerForm.value.customerNo;
  594. if (cNo) {
  595. listCustomerInfo({ customerNo: cNo }).then(cRes => {
  596. if (cRes.rows && cRes.rows.length > 0) {
  597. drawerForm.value.realCustomerId = cRes.rows[0].id;
  598. }
  599. });
  600. }
  601. // 加载完数据后,立即补全负责人和部门的文字名称
  602. enrichFormNames();
  603. fetchContactList(props.id);
  604. loadAnalysisData();
  605. loadFileList();
  606. } catch (error) {
  607. proxy.$modal.msgError("获取数据失败");
  608. } finally {
  609. loading.value = false;
  610. }
  611. };
  612. const loadFileList = async () => {
  613. if (drawerForm.value.fileNo) {
  614. try {
  615. const res = await listByIds(drawerForm.value.fileNo);
  616. detailFileList.value = (res.data || []).map(item => ({
  617. name: item.originalName || item.fileName || '',
  618. url: item.url,
  619. ossId: item.ossId,
  620. uploadTime: item.createTime
  621. }));
  622. } catch (e) {}
  623. } else {
  624. detailFileList.value = [];
  625. }
  626. };
  627. const fetchContactList = async (id) => {
  628. if (!id) return;
  629. contactLoading.value = true;
  630. try {
  631. const res = await listContact({ platformCode: String(id) });
  632. contactList.value = res.rows || [];
  633. } catch (e) {} finally { contactLoading.value = false; }
  634. };
  635. const loadDicts = () => {
  636. if (areaOptions.value.length === 0) {
  637. listProvinceWithCities().then(res => {
  638. const list = res.rows || [];
  639. if (list.length > 0) {
  640. areaOptions.value = handleTree(list, "id", "parentId");
  641. }
  642. });
  643. }
  644. };
  645. /** 构造树型结构数据 */
  646. function handleTree(data, id, parentId, children) {
  647. let config = {
  648. id: id || 'id',
  649. parentId: parentId || 'parentId',
  650. childrenList: children || 'children'
  651. };
  652. var childrenListMap = {};
  653. var nodeIds = {};
  654. var tree = [];
  655. for (let d of data) {
  656. let pId = d[config.parentId];
  657. if (childrenListMap[pId] == null) {
  658. childrenListMap[pId] = [];
  659. }
  660. nodeIds[d[config.id]] = d;
  661. childrenListMap[pId].push(d);
  662. }
  663. for (let d of data) {
  664. let pId = d[config.parentId];
  665. if (nodeIds[pId] == null) {
  666. tree.push(d);
  667. }
  668. }
  669. for (let t of tree) {
  670. adaptToChildrenList(t);
  671. }
  672. function adaptToChildrenList(o) {
  673. if (childrenListMap[o[config.id]] !== null) {
  674. o[config.childrenList] = childrenListMap[o[config.id]];
  675. }
  676. if (o[config.childrenList]) {
  677. for (let c of o[config.childrenList]) {
  678. adaptToChildrenList(c);
  679. }
  680. }
  681. }
  682. return tree;
  683. }
  684. loadDicts();
  685. // --- 项目联系人交互 ---
  686. const handleContactCommand = (command) => {
  687. if (command === 'associate') { handleAssociate(); }
  688. else if (command === 'new') { handleNewProjectContact(); }
  689. };
  690. const handleAssociate = () => {
  691. if (!drawerForm.value.customNo && !drawerForm.value.companyNo && !drawerForm.value.customerNo) {
  692. return ElMessage.warning("缺少客户信息,无法关联联系人");
  693. }
  694. associateVisible.value = true;
  695. associateLoading.value = true;
  696. // 统一使用客户编号进行查询
  697. const cNo = drawerForm.value.customNo || drawerForm.value.companyNo || drawerForm.value.customerNo;
  698. listContact({ customNo: cNo }).then(res => {
  699. const currentIds = contactList.value.map(c => c.id);
  700. associateList.value = (res.rows || []).filter(item => !currentIds.includes(item.id));
  701. }).finally(() => { associateLoading.value = false; });
  702. };
  703. const handleSelectionChange = (selection) => { selectedContacts.value = selection; };
  704. const confirmAssociate = async () => {
  705. if (selectedContacts.value.length === 0) return;
  706. try {
  707. const promises = selectedContacts.value.map(contact => {
  708. return updateContact({ ...contact, platformCode: String(props.id) });
  709. });
  710. await Promise.all(promises);
  711. ElMessage.success('关联成功');
  712. associateVisible.value = false;
  713. fetchContactList(props.id);
  714. } catch (e) {}
  715. };
  716. const handleNewProjectContact = () => {
  717. Object.assign(projectContactForm, {
  718. id: undefined, contactName: '', type: '1', gender: '0', age: '', nativePlace: '', birthday: '', remark: '',
  719. jobStatus: '1', phone: '', deptName: '', position: '', officePhone: '', officeRegion: [], addressDetail: '', jobContent: '',
  720. projectRole: null, isKeyPerson: 0, prStatus: '', familyStatus: '', hobby: '', characterTrait: '', isSmoke: '0', isDrink: '0',
  721. homeRegion: [], homeAddressDetail: ''
  722. });
  723. projectContactDrawerOpen.value = true;
  724. };
  725. const handleEditContact = (row) => {
  726. const contactId = row.contactId || row.id;
  727. if (!contactId) return;
  728. proxy.$modal.loading("加载中...");
  729. getContact(contactId).then(res => {
  730. const data = res.data || {};
  731. // 防御性合并:优先使用详情接口数据,但如果详情接口返回 null,则保留列表行中的有效数据
  732. const mergedData = { ...row };
  733. for (const key in data) {
  734. if (data[key] !== null && data[key] !== undefined && data[key] !== '') {
  735. mergedData[key] = data[key];
  736. }
  737. }
  738. Object.assign(projectContactForm, {
  739. id: data.id || row.id,
  740. contactName: mergedData.contactName,
  741. type: String(mergedData.type || '1'),
  742. gender: String(mergedData.gender || '0'),
  743. age: mergedData.age,
  744. nativePlace: mergedData.nativePlace,
  745. birthday: mergedData.birthday,
  746. remark: mergedData.remark,
  747. jobStatus: String(mergedData.jobStatus !== null && mergedData.jobStatus !== undefined ? mergedData.jobStatus : '1'),
  748. phone: mergedData.phone,
  749. deptName: mergedData.deptName,
  750. position: mergedData.position,
  751. officePhone: mergedData.officePhone,
  752. officeRegion: (mergedData.addressProvince != null && mergedData.addressCity != null && mergedData.addressCounty != null)
  753. ? [String(mergedData.addressProvince), String(mergedData.addressCity), String(mergedData.addressCounty)] : [],
  754. addressDetail: mergedData.addressDetail,
  755. jobContent: mergedData.jobContent,
  756. projectRole: mergedData.projectRole !== null && mergedData.projectRole !== undefined ? Number(mergedData.projectRole) : null,
  757. isKeyPerson: mergedData.isKeyPerson !== null && mergedData.isKeyPerson !== undefined ? Number(mergedData.isKeyPerson) : 0,
  758. prStatus: mergedData.prStatus,
  759. familyStatus: mergedData.familyStatus || mergedData.familyInfo || '',
  760. hobby: mergedData.hobby || mergedData.hobbies || '',
  761. characterTrait: mergedData.characterTrait || mergedData.character || '',
  762. isSmoke: String(mergedData.isSmoke !== null && mergedData.isSmoke !== undefined ? mergedData.isSmoke : '0'),
  763. isDrink: String(mergedData.isDrink !== null && mergedData.isDrink !== undefined ? mergedData.isDrink : '0'),
  764. homeRegion: (mergedData.homeProvinceId != null && mergedData.homeCityId != null && mergedData.homeAreaId != null)
  765. ? [Number(mergedData.homeProvinceId), Number(mergedData.homeCityId), Number(mergedData.homeAreaId)] : [],
  766. homeAddressDetail: mergedData.homeAddressDetail
  767. });
  768. // 区域级联处理 (适配代码转换)
  769. const findIdByCode = (code) => {
  770. if (!code) return null;
  771. const findInTree = (nodes, targetCode) => {
  772. for (const node of nodes) {
  773. if (String(node.areaCode) === String(targetCode)) return node.id;
  774. if (node.children) {
  775. const found = findInTree(node.children, targetCode);
  776. if (found) return found;
  777. }
  778. }
  779. return null;
  780. };
  781. return findInTree(areaOptions.value, code);
  782. };
  783. if (mergedData.addressProvince || mergedData.addressCity || mergedData.addressCounty) {
  784. const officeArr = [];
  785. if (mergedData.addressProvince) {
  786. const pid = /^\d+$/.test(mergedData.addressProvince) && mergedData.addressProvince.length < 5 ? mergedData.addressProvince : findIdByCode(mergedData.addressProvince);
  787. if (pid) officeArr.push(Number(pid));
  788. }
  789. if (mergedData.addressCity) {
  790. const cid = /^\d+$/.test(mergedData.addressCity) && mergedData.addressCity.length < 7 ? mergedData.addressCity : findIdByCode(mergedData.addressCity);
  791. if (cid) officeArr.push(Number(cid));
  792. }
  793. if (mergedData.addressCounty) {
  794. const aid = /^\d+$/.test(mergedData.addressCounty) && mergedData.addressCounty.length < 9 ? mergedData.addressCounty : findIdByCode(mergedData.addressCounty);
  795. if (aid) officeArr.push(Number(aid));
  796. }
  797. projectContactForm.officeRegion = officeArr;
  798. }
  799. projectContactDrawerOpen.value = true;
  800. }).finally(() => {
  801. proxy.$modal.closeLoading();
  802. });
  803. };
  804. const submitProjectContact = () => {
  805. projectContactFormRef.value.validate(async (valid) => {
  806. if (valid) {
  807. contactSubmitting.value = true;
  808. try {
  809. // 获取选中的 AreaCode 列表 (用于办公地址转换)
  810. const findCodeById = (id) => {
  811. const findInTree = (nodes, targetId) => {
  812. for (const node of nodes) {
  813. if (Number(node.id) === Number(targetId)) return node.areaCode;
  814. if (node.children) {
  815. const found = findInTree(node.children, targetId);
  816. if (found) return found;
  817. }
  818. }
  819. return null;
  820. };
  821. return findInTree(areaOptions.value, id);
  822. };
  823. const payload = {
  824. ...projectContactForm,
  825. platformCode: String(props.id),
  826. // 转换办公地址编码为代码 String
  827. addressProvince: projectContactForm.officeRegion?.[0] ? findCodeById(projectContactForm.officeRegion[0]) : null,
  828. addressCity: projectContactForm.officeRegion?.[1] ? findCodeById(projectContactForm.officeRegion[1]) : null,
  829. addressCounty: projectContactForm.officeRegion?.[2] ? findCodeById(projectContactForm.officeRegion[2]) : null,
  830. // 转换家庭地址为 ID Long
  831. homeProvinceId: projectContactForm.homeRegion?.[0] || null,
  832. homeCityId: projectContactForm.homeRegion?.[1] || null,
  833. homeAreaId: projectContactForm.homeRegion?.[2] || null,
  834. // 关联客户信息
  835. customerName: drawerForm.value.customName || drawerForm.value.customerName || '',
  836. customNo: drawerForm.value.customNo || drawerForm.value.customerNo || '',
  837. customerId: drawerForm.value.realCustomerId || null
  838. };
  839. if (projectContactForm.id) { await updateContact(payload); }
  840. else { await addContact(payload); }
  841. ElMessage.success('保存成功');
  842. projectContactDrawerOpen.value = false;
  843. fetchContactList(props.id);
  844. } catch (e) {} finally { contactSubmitting.value = false; }
  845. }
  846. });
  847. };
  848. const handleDeleteContact = (row) => {
  849. ElMessageBox.confirm(`确认删除联系人「${row.name || row.contactName}」吗?`, '提示', { type: 'warning' }).then(async () => {
  850. await delContact(row.id);
  851. ElMessage.success('操作成功');
  852. fetchContactList(props.id);
  853. });
  854. };
  855. // --- 结果分析交互 ---
  856. const loadAnalysisData = async () => {
  857. if (!drawerForm.value.projectNo) return;
  858. try {
  859. const res = await getSalesResultAnalyzeByObjectNo(drawerForm.value.projectNo);
  860. if (res.data) {
  861. Object.assign(analysisForm, {
  862. id: res.data.id, resultType: res.data.dealResult === 1 ? 'win' : 'lose',
  863. summary: res.data.winSumUp, loseReason: res.data.loseReason, fileNo: res.data.fileNo
  864. });
  865. if (analysisForm.fileNo) {
  866. const ossRes = await listByIds(analysisForm.fileNo);
  867. analysisFileList.value = (ossRes.data || []).map(i => ({
  868. name: i.originalName || i.fileName || '',
  869. url: i.url,
  870. ossId: i.ossId
  871. }));
  872. }
  873. }
  874. } catch (e) {}
  875. };
  876. const handleSaveAnalysis = useDebounceFn(async () => {
  877. const ossIds = analysisFileList.value.map(f => f.ossId).join(',');
  878. const payload = {
  879. id: analysisForm.id,
  880. objectNo: drawerForm.value.projectNo,
  881. dataType: 4, // 1:线索, 2:商机, 3:项目优选, 4:平台优选
  882. dealResult: analysisForm.resultType === 'win' ? 1 : 2,
  883. winSumUp: analysisForm.summary,
  884. loseReason: analysisForm.loseReason,
  885. fileNo: ossIds
  886. };
  887. try {
  888. if (analysisForm.id) { await updateSalesResultAnalyze(payload); }
  889. else { await addSalesResultAnalyze(payload); }
  890. ElMessage.success("保存成功");
  891. loadAnalysisData();
  892. } catch (e) {}
  893. }, 300);
  894. const handleAnalysisUploadSuccess = (res) => {
  895. if (res.code === 200) {
  896. analysisFileList.value.push({
  897. name: res.data.originalName || res.data.fileName || '',
  898. url: res.data.url,
  899. ossId: res.data.ossId
  900. });
  901. ElMessage.success("上传成功");
  902. }
  903. };
  904. const handleAnalysisDeleteFile = (index) => { analysisFileList.value.splice(index, 1); };
  905. // --- 附件管理交互 ---
  906. const handleUploadSuccess = async (res) => {
  907. if (res.code === 200) {
  908. const ossId = res.data.ossId;
  909. const currentFileNos = drawerForm.value.fileNo ? drawerForm.value.fileNo.split(',') : [];
  910. currentFileNos.push(ossId);
  911. const newFileNo = currentFileNos.join(',');
  912. try {
  913. await updatePlatformSelection({ id: props.id, fileNo: newFileNo });
  914. drawerForm.value.fileNo = newFileNo;
  915. loadFileList();
  916. ElMessage.success('上传成功');
  917. } catch (e) {}
  918. }
  919. };
  920. const downloadFile = (file) => {
  921. if (file.url) {
  922. if (proxy.$download && proxy.$download.resource) {
  923. proxy.$download.resource(file.url);
  924. } else {
  925. window.open(file.url, '_blank');
  926. }
  927. }
  928. };
  929. const handleDeleteFile = (row) => {
  930. ElMessageBox.confirm(`确认删除附件「${row.name}」吗?`, '提示', { type: 'warning' }).then(async () => {
  931. const currentFileNos = drawerForm.value.fileNo ? drawerForm.value.fileNo.split(',') : [];
  932. const newFileNos = currentFileNos.filter(id => String(id) !== String(row.ossId));
  933. const newFileNoStr = newFileNos.join(',');
  934. try {
  935. await updatePlatformSelection({ id: props.id, fileNo: newFileNoStr });
  936. drawerForm.value.fileNo = newFileNoStr;
  937. loadFileList();
  938. ElMessage.success('删除成功');
  939. } catch (e) {
  940. proxy.$modal.msgError("删除失败");
  941. }
  942. }).catch(() => {});
  943. };
  944. const handleStepClick = async (status) => {
  945. // 再次点击取消:如果点击已激活步骤则设为 null,否则设为目标步骤
  946. const newStatus = String(drawerForm.value.projectStatus) === String(status) ? null : status;
  947. try {
  948. await updatePlatformSelection({ id: props.id, projectStatus: newStatus });
  949. drawerForm.value.projectStatus = newStatus;
  950. proxy.$modal.msgSuccess(newStatus ? "进度更新成功" : "进度已取消");
  951. emit('success');
  952. } catch (err) {
  953. console.error('更新进度失败:', err);
  954. }
  955. };
  956. const handleEdit = () => {
  957. emit('edit', props.id);
  958. };
  959. const handleDownload = (row) => {
  960. if (!row.fileUrl) return proxy.$modal.msgError("文件地址不存在");
  961. window.open(import.meta.env.VITE_APP_BASE_API + row.fileUrl, '_blank');
  962. };
  963. const submitAnalysis = async () => {
  964. await updatePlatformSelection(drawerForm.value);
  965. proxy.$modal.msgSuccess("分析结果保存成功");
  966. };
  967. const openLink = (link) => {
  968. if (link) {
  969. if (!link.startsWith('http')) link = 'http://' + link;
  970. window.open(link, '_blank');
  971. }
  972. };
  973. const formatDate = (date) => date ? proxy.parseTime(date, '{y}-{m}-{d}') : '';
  974. const getStatusLabel = (s) => props.options.status?.find(o => String(o.value) === String(s))?.label || '';
  975. const getProjectRoleLabel = (val) => {
  976. return projectRoleOptions.value.find(o => String(o.value) === String(val))?.label || val || '';
  977. };
  978. const displayProfessionName = computed(() => {
  979. const val = drawerForm.value.profession || drawerForm.value.materialCategory;
  980. return props.options.material?.find(o => String(o.value) === String(val))?.label || '';
  981. });
  982. const displayShortlistedName = computed(() => {
  983. const val = drawerForm.value.shortlistedType || drawerForm.value.finalizationType;
  984. return props.options.shortlisted?.find(o => String(o.value) === String(val))?.label || '';
  985. });
  986. const displayCompanyName = computed(() => {
  987. const val = drawerForm.value.companyNo;
  988. return props.options.company?.find(c => String(c.companyCode) === String(val) || String(c.id) === String(val))?.companyName || '';
  989. });
  990. const displayProjectLevelName = computed(() => props.options.level?.find(o => String(o.value) === String(drawerForm.value.projectLevel))?.label || '');
  991. const displayProjectTypeName = computed(() => props.options.type?.find(o => String(o.value) === String(drawerForm.value.businessType))?.label || '');
  992. const displayIndustryName = computed(() => {
  993. // 优先取确认是文字名称的字段(排除纯数字 ID)
  994. if (drawerForm.value.industryName && isNaN(Number(drawerForm.value.industryName))) return drawerForm.value.industryName;
  995. if (drawerForm.value.industryCategoryName && isNaN(Number(drawerForm.value.industryCategoryName))) return drawerForm.value.industryCategoryName;
  996. if (drawerForm.value.industry && isNaN(Number(drawerForm.value.industry))) return drawerForm.value.industry;
  997. // 否则通过 ID 查找
  998. const professionId = drawerForm.value.profession || drawerForm.value.industry;
  999. if (professionId && props.options.industryList) {
  1000. const flatten = (list) => {
  1001. let res = [];
  1002. list.forEach(i => {
  1003. res.push(i);
  1004. if (i.children) res = res.concat(flatten(i.children));
  1005. });
  1006. return res;
  1007. };
  1008. const flatList = flatten(props.options.industryList);
  1009. const found = flatList.find(i => String(i.id) === String(professionId))?.industryCategoryName || '';
  1010. if (found) return found;
  1011. }
  1012. return '';
  1013. });
  1014. const displayDeptName = computed(() => {
  1015. // 第一优先:后端直接返回的文字部门名
  1016. const name = drawerForm.value.deptName || drawerForm.value.createDeptName || '';
  1017. if (name && isNaN(Number(name))) return name;
  1018. // 第二兑底:通过部门 ID 在父组件传入的部门树中反查
  1019. const deptId = drawerForm.value.deptNo || drawerForm.value.deptId ||
  1020. drawerForm.value.createDept || (isNaN(Number(name)) ? '' : name);
  1021. if (deptId && props.options?.dept?.length) {
  1022. const flatten = (list) => {
  1023. let res = [];
  1024. list.forEach(i => { res.push(i); if (i.children) res = res.concat(flatten(i.children)); });
  1025. return res;
  1026. };
  1027. const found = flatten(props.options.dept).find(d => String(d.id) === String(deptId));
  1028. if (found) return found.label || found.deptName || '';
  1029. }
  1030. return '';
  1031. });
  1032. </script>
  1033. <style scoped lang="scss">
  1034. .platform-detail-drawer {
  1035. :deep(.el-drawer__body) { padding: 0; background: #fff; overflow: hidden; display: flex; flex-direction: column; }
  1036. }
  1037. .detail-header {
  1038. padding: 16px 24px; background: #fff; display: flex; justify-content: space-between; align-items: center; z-index: 10;
  1039. .project-title { font-size: 16px; color: #333; font-weight: normal; }
  1040. .header-right { display: flex; align-items: center; gap: 16px; .close-btn { font-size: 20px; color: #86909c; cursor: pointer; &:hover { color: #333; } } }
  1041. }
  1042. .detail-main-scroll {
  1043. flex: 1;
  1044. overflow-y: auto;
  1045. padding: 0 24px 24px;
  1046. &::-webkit-scrollbar { display: none; }
  1047. -ms-overflow-style: none;
  1048. scrollbar-width: none;
  1049. }
  1050. .drawer-body-custom {
  1051. &::-webkit-scrollbar { display: none; }
  1052. -ms-overflow-style: none;
  1053. scrollbar-width: none;
  1054. }
  1055. .project-progress-card {
  1056. background: #fff; padding: 0 0 24px 0; margin-bottom: 0;
  1057. .section-title { font-size: 13px; color: #333; margin-bottom: 12px; font-weight: normal; }
  1058. .chevron-steps {
  1059. display: flex; align-items: center; gap: 0; padding: 2px 0;
  1060. .chevron-step {
  1061. 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;
  1062. margin-right: -10px; // 负边距实现重叠
  1063. clip-path: polygon(calc(100% - 10px) 0%, 100% 50%, calc(100% - 10px) 100%, 0% 100%, 10px 50%, 0% 0%);
  1064. &:first-child {
  1065. border-radius: 4px 0 0 4px;
  1066. clip-path: polygon(calc(100% - 10px) 0%, 100% 50%, calc(100% - 10px) 100%, 0% 100%, 0% 0%);
  1067. }
  1068. &:last-child {
  1069. border-radius: 0 4px 4px 0;
  1070. margin-right: 0;
  1071. clip-path: polygon(100% 0%, 100% 100%, 0% 100%, 10px 50%, 0% 0%);
  1072. }
  1073. &.is-active { background-color: #00B881; color: #fff; z-index: 2; font-weight: 500 !important; }
  1074. &.is-finished { background-color: #E6F8F3; color: #00B881; z-index: 1; }
  1075. &:hover:not(.is-active) { background-color: #E5E6EB; z-index: 3; }
  1076. }
  1077. }
  1078. .progress-footer { display: none; }
  1079. }
  1080. .detail-content-layout { display: flex; gap: 24px; align-items: stretch; margin-top: 0; }
  1081. .content-left-panel { flex: 6.5; display: flex; flex-direction: column; overflow: hidden; }
  1082. .content-right-panel { flex: 3.5; background: #fff; overflow: hidden; display: flex; flex-direction: column; }
  1083. .detail-content-card {
  1084. background: #fff; padding: 0; flex: 1; display: flex; flex-direction: column;
  1085. }
  1086. .custom-tabs {
  1087. :deep(.el-tabs__header) {
  1088. margin: 0 !important;
  1089. padding: 0 16px !important;
  1090. background: #fff !important;
  1091. height: 48px !important;
  1092. box-sizing: border-box !important;
  1093. position: relative !important;
  1094. border-bottom: 1px solid #dcdfe6 !important;
  1095. }
  1096. :deep(.el-tabs__nav-wrap::after) { display: none !important; }
  1097. :deep(.el-tabs__item) {
  1098. height: 48px !important;
  1099. line-height: 48px !important;
  1100. font-size: 14px;
  1101. color: #666;
  1102. &.is-active { color: #409eff; font-weight: 500 !important; }
  1103. }
  1104. :deep(.el-tabs__active-bar) {
  1105. height: 2px !important;
  1106. border-radius: 1px !important;
  1107. bottom: 0 !important;
  1108. z-index: 5;
  1109. }
  1110. :deep(.el-tabs__content) { padding: 24px; }
  1111. }
  1112. .content-right-panel {
  1113. flex: 3; background: #fff; overflow: hidden; display: flex; flex-direction: column;
  1114. :deep(.el-tabs__header) {
  1115. margin: 0 !important;
  1116. padding: 0 16px !important;
  1117. background: #fff !important;
  1118. height: 48px !important;
  1119. box-sizing: border-box !important;
  1120. position: relative !important;
  1121. border-bottom: 1px solid #dcdfe6 !important;
  1122. }
  1123. :deep(.el-tabs__item) {
  1124. height: 48px !important;
  1125. line-height: 48px !important;
  1126. padding: 0 10px !important;
  1127. min-width: auto !important;
  1128. }
  1129. :deep(.el-tabs__nav-wrap::after) { display: none !important; }
  1130. :deep(.el-tabs__active-bar) {
  1131. height: 2px !important;
  1132. bottom: 0 !important;
  1133. z-index: 5;
  1134. }
  1135. /* 隐藏右侧可能出现的滚动箭头 */
  1136. :deep(.el-tabs__nav-prev), :deep(.el-tabs__nav-next) { display: none !important; }
  1137. :deep(.el-tabs__nav-scroll) { overflow: visible !important; }
  1138. }
  1139. .info-section {
  1140. .info-section-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; .title { font-size: 15px; color: #409eff; font-weight: normal; } }
  1141. .custom-desc {
  1142. :deep(.el-descriptions__label) { color: #86909c; font-weight: normal; width: auto; background: transparent !important; padding-bottom: 12px; &::after { content: ":"; } }
  1143. :deep(.el-descriptions__content) { color: #333; padding-bottom: 12px; }
  1144. }
  1145. }
  1146. .mt-24 { margin-top: 24px; }
  1147. :deep(.hidden-label) { display: none !important; }
  1148. :deep(.hidden-value) { display: none !important; }
  1149. .no-bold-label {
  1150. :deep(.el-form-item__label) { font-weight: normal !important; color: #666; }
  1151. :deep(.el-form-item) { margin-bottom: 18px !important; }
  1152. }
  1153. :deep(*) { font-weight: normal !important; }
  1154. .mt-20 { margin-top: 20px; }
  1155. .tab-toolbar { margin-bottom: 16px; display: flex; justify-content: flex-end; }
  1156. .contact-tab-content, .analysis-tab-content, .files-tab-content { min-height: 400px; padding: 10px 0; }
  1157. .analysis-form {
  1158. max-width: 900px;
  1159. .analysis-row { display: flex; align-items: center; margin-bottom: 24px; .form-label { width: 80px; color: #4E5969; } .result-radio { margin-left: 12px; } }
  1160. .summary-title { font-size: 14px; color: #4E5969; margin-bottom: 12px; }
  1161. .summary-textarea-wrap { margin-bottom: 24px; }
  1162. .attachment-section-title { font-size: 14px; color: #4E5969; margin-bottom: 12px; }
  1163. .upload-area { margin-bottom: 12px; }
  1164. }
  1165. .empty-tab { color: #86909c; font-size: 13px; text-align: center; padding: 40px; }
  1166. .custom-table { :deep(th.el-table__cell) { background-color: #f7f8fa !important; color: #4e5969; font-weight: normal; } }
  1167. /* 抽屉标准样式 */
  1168. .section-group-title-bar { font-size: 14px; color: #409eff; padding: 8px 16px; background-color: #F8F9FA; margin-bottom: 20px; border-radius: 4px; }
  1169. .form-section-group { margin-bottom: 32px; }
  1170. .dialog-footer { display: flex; justify-content: flex-end; gap: 12px; padding-top: 20px; }
  1171. </style>