index.vue 55 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811
  1. <template>
  2. <div class="delivery-project-page">
  3. <!-- 查询表单 -->
  4. <div class="query-form-container">
  5. <div class="query-form-basic">
  6. <el-form class="query-form-fields" :inline="true" :model="queryForm" size="small">
  7. <el-form-item label="事件标题">
  8. <el-input v-model="queryForm.deliveryEventTitle" clearable placeholder="请输入" style="width: 180px" />
  9. </el-form-item>
  10. <el-form-item label="事件状态">
  11. <el-select
  12. v-model="queryForm.deliveryEventStatus"
  13. collapse-tags
  14. multiple
  15. placeholder="请选择"
  16. style="width: 140px">
  17. <el-option
  18. v-for="dict in deliveryEventStatusOptions"
  19. :key="dict.key"
  20. :label="dict.value"
  21. :value="dict.key" />
  22. </el-select>
  23. </el-form-item>
  24. <el-form-item label="反馈人">
  25. <el-input v-model="queryForm.feedbackReporter" clearable placeholder="请输入" style="width: 120px" />
  26. </el-form-item>
  27. <el-form-item label="负责人">
  28. <el-select
  29. v-model="queryForm.opsUserName"
  30. class="query-select--owner"
  31. clearable
  32. collapse-tags
  33. filterable
  34. :loading="opsUsersLoading"
  35. multiple
  36. placeholder="请选择"
  37. remote
  38. :remote-method="remoteFetchOpsUsers"
  39. reserve-keyword
  40. @visible-change="handleOpsUserVisibleChange">
  41. <el-option v-for="u in opsUserOptions" :key="u.value" :label="u.label" :value="u.label" />
  42. </el-select>
  43. </el-form-item>
  44. <el-form-item label="反馈时间">
  45. <el-date-picker
  46. v-model="queryForm.feedbackDateRange"
  47. end-placeholder="结束日期"
  48. range-separator="至"
  49. start-placeholder="开始日期"
  50. style="width: 300px"
  51. type="daterange"
  52. value-format="yyyy-MM-dd" />
  53. </el-form-item>
  54. </el-form>
  55. <div class="query-form-actions">
  56. <el-button icon="el-icon-search" type="primary" @click="handleSearch">查询</el-button>
  57. <el-button icon="el-icon-refresh-right" @click="handleReset">重置</el-button>
  58. <el-button type="text" @click="showAdvanced = !showAdvanced">
  59. {{ showAdvanced ? '收起' : '高级筛选' }}
  60. <i :class="showAdvanced ? 'el-icon-arrow-up' : 'el-icon-arrow-down'" />
  61. </el-button>
  62. <el-divider direction="vertical" />
  63. <el-button icon="el-icon-plus" type="success" @click="handleAdd">新增</el-button>
  64. </div>
  65. </div>
  66. <!-- 高级查询条件 -->
  67. <div v-show="showAdvanced" class="query-form-advanced">
  68. <el-form :inline="true" :model="queryForm" size="small">
  69. <el-form-item label="事件类型">
  70. <el-select
  71. v-model="queryForm.deliveryEventType"
  72. collapse-tags
  73. multiple
  74. placeholder="请选择"
  75. style="width: 160px">
  76. <el-option
  77. v-for="dict in deliveryEventTypeOptions"
  78. :key="dict.key"
  79. :label="dict.value"
  80. :value="dict.key" />
  81. </el-select>
  82. </el-form-item>
  83. <el-form-item label="事件结果">
  84. <el-select
  85. v-model="queryForm.deliveryEventResult"
  86. collapse-tags
  87. multiple
  88. placeholder="请选择"
  89. style="width: 140px">
  90. <el-option
  91. v-for="dict in deliveryEventResultOptions"
  92. :key="dict.key"
  93. :label="dict.value"
  94. :value="dict.key" />
  95. </el-select>
  96. </el-form-item>
  97. <el-form-item label="事件描述">
  98. <el-input v-model="queryForm.deliveryEventDesc" clearable placeholder="请输入" style="width: 180px" />
  99. </el-form-item>
  100. <el-form-item label="处理时间">
  101. <el-date-picker
  102. v-model="queryForm.completeDateRange"
  103. end-placeholder="结束日期"
  104. range-separator="至"
  105. start-placeholder="开始日期"
  106. style="width: 300px"
  107. type="daterange"
  108. value-format="yyyy-MM-dd" />
  109. </el-form-item>
  110. </el-form>
  111. </div>
  112. </div>
  113. <!-- 主体内容 -->
  114. <div class="main-content">
  115. <!-- 左侧项目筛选 -->
  116. <div :class="['project-sidebar', { collapsed: sidebarCollapsed }]">
  117. <div class="sidebar-header">
  118. <div v-if="!sidebarCollapsed" class="sidebar-title-group">
  119. <span class="sidebar-title">项目列表</span>
  120. <button
  121. :class="['sidebar-action-btn', { active: showSidebarFilters }]"
  122. :title="showSidebarFilters ? '隐藏项目筛选' : '显示项目筛选'"
  123. type="button"
  124. @click="toggleSidebarFilters">
  125. <i class="el-icon-search" />
  126. </button>
  127. </div>
  128. <div class="sidebar-actions">
  129. <button
  130. class="collapse-trigger"
  131. :title="sidebarCollapsed ? '展开项目列表' : '折叠项目列表'"
  132. type="button"
  133. @click="toggleSidebar">
  134. <i :class="sidebarCollapsed ? 'el-icon-d-arrow-right' : 'el-icon-d-arrow-left'" />
  135. </button>
  136. </div>
  137. </div>
  138. <div v-if="sidebarCollapsed" class="sidebar-collapsed-label">项目列表</div>
  139. <template v-if="!sidebarCollapsed">
  140. <template v-if="showSidebarFilters">
  141. <div class="project-status-filter">
  142. <el-radio-group v-model="projectStatusFilter" size="small" @change="handleProjectStatusChange">
  143. <el-radio-button label="">全部</el-radio-button>
  144. <el-radio-button label="pending">待分配</el-radio-button>
  145. <el-radio-button label="delivering">交付中</el-radio-button>
  146. <el-radio-button label="delivered">已验收</el-radio-button>
  147. </el-radio-group>
  148. </div>
  149. <el-input
  150. v-model="projectSearch"
  151. class="project-search"
  152. clearable
  153. placeholder="搜索项目/销售/交付"
  154. prefix-icon="el-icon-search"
  155. size="small"
  156. @input="onProjectSearchDebounced" />
  157. </template>
  158. <div ref="projectList" class="project-list">
  159. <div
  160. v-if="allProjectOption"
  161. :class="['project-item', 'project-item--all', { active: selectedProject === allProjectOption.id }]"
  162. @click="selectProject(allProjectOption.id)">
  163. <div>
  164. <div class="project-overview-label">{{ allProjectOption.name }}</div>
  165. <div class="project-overview-desc">查看全部项目交付进度</div>
  166. </div>
  167. </div>
  168. <div
  169. v-for="project in projectCards"
  170. :key="project.id"
  171. :class="['project-card', { active: selectedProject === project.id }]"
  172. @click="selectProject(project.id, project)">
  173. <div class="project-card-top">
  174. <div class="project-card-tags">
  175. <span class="project-contract clickable" @click.stop="handleViewProjectInfo(project)">
  176. {{ project.contractNo || '-' }}
  177. </span>
  178. <span class="project-line-tag">{{ getProductLineLabel(project.productLine) }}</span>
  179. </div>
  180. <span :class="['project-status-tag', 'project-status-tag--' + project.status]">
  181. {{ selectDictLabel(projectStatusOptions, project.status) }}
  182. </span>
  183. </div>
  184. <div class="project-card-title" :title="project.name">{{ project.name }}</div>
  185. <div class="project-card-meta">
  186. <div class="project-card-meta-item">
  187. <i class="el-icon-user project-card-meta-icon" title="销售负责人" />
  188. <span class="project-card-meta-value" :title="project.salesOwner || '-'">
  189. {{ project.salesOwner || '-' }}
  190. </span>
  191. </div>
  192. <div class="project-card-meta-item">
  193. <i
  194. class="el-icon-s-custom project-card-meta-icon project-card-meta-icon--delivery"
  195. title="交付负责人" />
  196. <span class="project-card-meta-value" :title="project.deliveryOwner || '-'">
  197. {{ project.deliveryOwner || '-' }}
  198. </span>
  199. <i
  200. v-if="project.deliveryOwner && canManageDelivery"
  201. class="el-icon-refresh-right reassign-btn"
  202. title="改派"
  203. @click.stop="handleReassign(project)" />
  204. </div>
  205. </div>
  206. <i v-if="selectedProject === project.id" class="el-icon-check project-card-check" />
  207. </div>
  208. <div v-if="projectLoading" class="project-list-loading">
  209. <i class="el-icon-loading" />
  210. 加载中...
  211. </div>
  212. <div v-else-if="!projectHasMore && projects.length > 1" class="project-list-end">没有更多了</div>
  213. </div>
  214. </template>
  215. </div>
  216. <!-- 右侧数据表格 -->
  217. <div class="table-container">
  218. <div class="table-scroll-wrapper">
  219. <el-table
  220. v-loading="loading"
  221. border
  222. class="delivery-project-table"
  223. :data="tableData"
  224. :fit="false"
  225. height="100%"
  226. stripe
  227. style="width: 100%"
  228. @row-dblclick="handleRowClick">
  229. <el-table-column align="center" type="index" width="50" />
  230. <el-table-column label="事件标题" min-width="320" show-overflow-tooltip>
  231. <template slot-scope="{ row }">
  232. <span class="event-title-text">
  233. {{ row.deliveryEventTitle || row.delivery_event_title || '-' }}
  234. </span>
  235. </template>
  236. </el-table-column>
  237. <el-table-column label="项目名称" min-width="200" show-overflow-tooltip>
  238. <template slot-scope="{ row }">
  239. {{ row.projectName || row.project_name || '-' }}
  240. </template>
  241. </el-table-column>
  242. <el-table-column
  243. label="事件类型"
  244. :render-header="renderSortableHeader('事件类型', 'deliveryEventType')"
  245. width="140">
  246. <template slot-scope="{ row }">
  247. <el-tag size="small" type="info">
  248. {{ selectDictLabel(deliveryEventTypeOptions, row.deliveryEventType || row.delivery_event_type) }}
  249. </el-tag>
  250. </template>
  251. </el-table-column>
  252. <el-table-column
  253. label="事件状态"
  254. :render-header="renderSortableHeader('事件状态', 'deliveryEventStatus')"
  255. width="120">
  256. <template slot-scope="{ row }">
  257. <el-tag
  258. size="small"
  259. :type="getDeliveryEventStatusTagType(row.deliveryEventStatus || row.delivery_event_status)">
  260. {{
  261. selectDictLabel(deliveryEventStatusOptions, row.deliveryEventStatus || row.delivery_event_status)
  262. }}
  263. </el-tag>
  264. </template>
  265. </el-table-column>
  266. <el-table-column
  267. label="事件结果"
  268. :render-header="renderSortableHeader('事件结果', 'deliveryEventResult')"
  269. width="120">
  270. <template slot-scope="{ row }">
  271. {{ selectDictLabel(deliveryEventResultOptions, row.deliveryEventResult || row.delivery_event_result) }}
  272. </template>
  273. </el-table-column>
  274. <el-table-column
  275. label="负责人"
  276. :render-header="renderSortableHeader('负责人', 'opsUserName')"
  277. show-overflow-tooltip
  278. width="130">
  279. <template slot-scope="{ row }">
  280. {{ row.opsUserName || row.ops_user_name || '-' }}
  281. </template>
  282. </el-table-column>
  283. <el-table-column
  284. label="处理时间"
  285. :render-header="renderSortableHeader('处理时间', 'completeTime')"
  286. width="180">
  287. <template slot-scope="{ row }">
  288. {{ formatEventTime(row.completeTime || row.complete_time) }}
  289. </template>
  290. </el-table-column>
  291. <el-table-column label="处理说明" min-width="240" show-overflow-tooltip>
  292. <template slot-scope="{ row }">
  293. {{ row.completeDesc || row.complete_desc || '-' }}
  294. </template>
  295. </el-table-column>
  296. <el-table-column label="是否现场" width="90">
  297. <template slot-scope="{ row }">
  298. {{ selectDictLabel(onSiteOptions, row.onSite || row.on_site) }}
  299. </template>
  300. </el-table-column>
  301. <el-table-column
  302. label="反馈人"
  303. :render-header="renderSortableHeader('反馈人', 'feedbackReporter')"
  304. show-overflow-tooltip
  305. width="130">
  306. <template slot-scope="{ row }">
  307. {{ row.feedbackReporter || row.feedback_reporter || '-' }}
  308. </template>
  309. </el-table-column>
  310. <el-table-column
  311. label="反馈时间"
  312. :render-header="renderSortableHeader('反馈时间', 'feedbackDate')"
  313. width="180">
  314. <template slot-scope="{ row }">
  315. {{ formatEventTime(row.feedbackDate || row.feedback_date) }}
  316. </template>
  317. </el-table-column>
  318. <el-table-column label="反馈来源" width="100">
  319. <template slot-scope="{ row }">
  320. {{ selectDictLabel(feedbackSourceOptions, row.feedbackSource || row.feedback_source) }}
  321. </template>
  322. </el-table-column>
  323. <el-table-column fixed="right" header-align="center" label="操作" width="200">
  324. <template slot-scope="{ row }">
  325. <el-button
  326. v-if="String(row.deliveryEventStatus || row.delivery_event_status) !== '30'"
  327. size="mini"
  328. type="text"
  329. @click.stop="handleEdit(row)">
  330. 编辑
  331. </el-button>
  332. <el-button
  333. v-if="['10', '20'].includes(String(row.deliveryEventStatus || row.delivery_event_status))"
  334. size="mini"
  335. type="text"
  336. @click.stop="handleProcess(row)">
  337. 处理
  338. </el-button>
  339. <el-button v-if="showProgressButton(row)" size="mini" type="text" @click.stop="handleProgress(row)">
  340. 进展
  341. </el-button>
  342. <el-button
  343. v-if="['10', '20'].includes(String(row.deliveryEventStatus || row.delivery_event_status))"
  344. size="mini"
  345. style="color: #f56c6c"
  346. type="text"
  347. @click.stop="handleCancelEvent(row)">
  348. 作废
  349. </el-button>
  350. </template>
  351. </el-table-column>
  352. </el-table>
  353. </div>
  354. <!-- 分页 -->
  355. <div class="pagination-wrapper">
  356. <el-pagination
  357. background
  358. :current-page.sync="queryForm.pageNum"
  359. layout="total, sizes, prev, pager, next, jumper"
  360. :page-size.sync="queryForm.pageSize"
  361. :page-sizes="[10, 20, 50, 100]"
  362. :total="total"
  363. @current-change="handleCurrentChange"
  364. @size-change="handleSizeChange" />
  365. </div>
  366. </div>
  367. </div>
  368. <!-- 编辑弹窗 -->
  369. <delivery-project-event-edit
  370. :data="editData"
  371. :project-id="selectedProject"
  372. :visible.sync="editDialogVisible"
  373. @refresh="handleRefreshEvent" />
  374. <!-- 详情弹窗 -->
  375. <delivery-project-event-detail
  376. :data="currentRow"
  377. :process-mode="detailMode === 'process'"
  378. :read-only="detailMode === 'view'"
  379. :show-close-event-dialog.sync="showCloseEventDialog"
  380. :visible.sync="detailVisible"
  381. @refresh="handleRefreshEvent" />
  382. <!-- 作废事件弹窗 -->
  383. <el-dialog :close-on-click-modal="false" title="作废事件" :visible.sync="cancelEventDialogVisible" width="500px">
  384. <el-form
  385. ref="cancelEventForm"
  386. label-width="100px"
  387. :model="cancelEventForm"
  388. :rules="cancelEventRules"
  389. size="small">
  390. <el-form-item label="作废原因" prop="cancelReason">
  391. <el-input v-model="cancelEventForm.cancelReason" placeholder="请输入作废原因..." :rows="4" type="textarea" />
  392. </el-form-item>
  393. <el-form-item label="附件上传">
  394. <el-upload
  395. action=""
  396. :auto-upload="false"
  397. :file-list="cancelEventFileList"
  398. :limit="5"
  399. :multiple="true"
  400. :on-change="handleCancelEventFileChange"
  401. :on-remove="handleCancelEventFileRemove">
  402. <el-button size="small" type="primary">选择文件</el-button>
  403. <div slot="tip" class="el-upload__tip">最多上传5个文件,单个文件不超过20MB</div>
  404. </el-upload>
  405. </el-form-item>
  406. </el-form>
  407. <div slot="footer">
  408. <el-button size="small" @click="handleCancelEventDialogCancel">取消</el-button>
  409. <el-button :loading="submitLoading" size="small" type="primary" @click="handleSaveCancelEvent">确定</el-button>
  410. </div>
  411. </el-dialog>
  412. <!-- 项目负责人指派/改派弹窗 -->
  413. <delivery-project-assign
  414. :data="assignDialogData"
  415. :visible.sync="assignDialogVisible"
  416. @success="handleAssignSuccess" />
  417. <!-- 项目信息弹窗 -->
  418. <project-info-dialog :project-id="selectedProjectId" :visible.sync="projectInfoDialogVisible" />
  419. <!-- 研发任务进展弹窗 -->
  420. <delivery-project-event-progress :event="progressEventRow" :visible.sync="progressDialogVisible" />
  421. </div>
  422. </template>
  423. <script>
  424. import deliveryProjectApi from '@/api/devops/deliveryProject'
  425. import deliveryProjectEventApi from '@/api/devops/deliveryProjectEvent'
  426. import userApi from '@/api/system/user'
  427. import { mapGetters } from 'vuex'
  428. import { DEVOPS_DEV_DEPT_ID } from '@/config/devops.config'
  429. import DeliveryProjectEventEdit from './components/DeliveryProjectEventEdit'
  430. import DeliveryProjectEventDetail from './components/DeliveryProjectEventDetail'
  431. import DeliveryProjectEventProgress from './components/DeliveryProjectEventProgress'
  432. import DeliveryProjectAssign from './components/DeliveryProjectAssign'
  433. import ProjectInfoDialog from '../components/ProjectInfoDialog'
  434. import { parseTime } from '@/utils'
  435. import debounce from 'lodash/debounce'
  436. import { uploadFileToRichtextServer } from '@/utils/richtextUpload'
  437. import { sanitizeHtml } from '@/utils/safeHtml'
  438. import { deliveryEventStatusTagTypes, getTagType } from '@/config/devopsTagTypes'
  439. import dictApi from '@/api/system/dict'
  440. export default {
  441. name: 'DeliveryProject',
  442. components: {
  443. DeliveryProjectEventEdit,
  444. DeliveryProjectEventDetail,
  445. DeliveryProjectEventProgress,
  446. DeliveryProjectAssign,
  447. ProjectInfoDialog,
  448. },
  449. data() {
  450. return {
  451. queryForm: {
  452. pageNum: 1,
  453. pageSize: 20,
  454. projectId: '',
  455. deliveryEventTitle: '',
  456. deliveryEventDesc: '',
  457. deliveryEventType: [],
  458. deliveryEventStatus: [],
  459. deliveryEventResult: [],
  460. opsUserName: '',
  461. feedbackReporter: '',
  462. feedbackDateRange: [],
  463. completeDateRange: [],
  464. sortFields: [],
  465. },
  466. showAdvanced: false,
  467. opsUserOptions: [],
  468. opsUsersLoading: false,
  469. tableData: [],
  470. total: 0,
  471. loading: false,
  472. submitLoading: false,
  473. sidebarCollapsed: false,
  474. showSidebarFilters: true,
  475. projectSearch: '',
  476. projectPageNum: 1,
  477. projectPageSize: 20,
  478. projectTotal: 0,
  479. projectLoading: false,
  480. projectHasMore: true,
  481. projectStatusFilter: 'delivering',
  482. selectedProject: '',
  483. projects: [{ id: '', name: '全部' }],
  484. editDialogVisible: false,
  485. editData: null,
  486. productLineDict: [],
  487. projectStatusOptions: [],
  488. deliveryEventTypeOptions: [],
  489. deliveryEventStatusOptions: [],
  490. deliveryEventResultOptions: [],
  491. feedbackSourceOptions: [],
  492. onSiteOptions: [],
  493. detailVisible: false,
  494. detailMode: 'view',
  495. currentRow: null,
  496. showCloseEventDialog: false,
  497. // 作废事件弹窗相关数据
  498. cancelEventDialogVisible: false,
  499. cancelEventForm: {
  500. cancelReason: '',
  501. },
  502. cancelEventRules: {
  503. cancelReason: [{ required: true, message: '请输入作废原因', trigger: 'blur' }],
  504. },
  505. cancelEventFileList: [],
  506. cancelEventUploadFiles: [],
  507. // 项目负责人指派/改派弹窗相关数据
  508. assignDialogVisible: false,
  509. assignDialogData: null,
  510. // 项目信息弹窗
  511. projectInfoDialogVisible: false,
  512. selectedProjectId: '',
  513. // 进展弹窗相关数据
  514. progressDialogVisible: false,
  515. progressEventRow: null,
  516. }
  517. },
  518. computed: {
  519. ...mapGetters({
  520. roleKeys: 'user/roleKeys',
  521. }),
  522. canManageDelivery() {
  523. return (
  524. this.roleKeys.includes('ResearchAndDevelopmentDirector') ||
  525. this.roleKeys.includes('ResearchAndDevelopmentSupervisor')
  526. )
  527. },
  528. filteredProjects() {
  529. const statusFilter = this.projectStatusFilter
  530. return this.projects.filter((project) => {
  531. if (project.id === '') return true
  532. // 状态筛选
  533. if (statusFilter) {
  534. let statusList = []
  535. if (statusFilter === 'pending') {
  536. statusList = ['10']
  537. } else if (statusFilter === 'delivering') {
  538. statusList = ['20', '30', '40']
  539. } else if (statusFilter === 'delivered') {
  540. statusList = ['50']
  541. } else {
  542. statusList = statusFilter.split(',')
  543. }
  544. if (!statusList.includes(project.status)) {
  545. return false
  546. }
  547. } else {
  548. if (project.status === '90') {
  549. return false
  550. }
  551. }
  552. return true
  553. })
  554. },
  555. allProjectOption() {
  556. return this.filteredProjects.find((project) => project.id === '')
  557. },
  558. projectCards() {
  559. return this.filteredProjects.filter((project) => project.id !== '')
  560. },
  561. },
  562. created() {
  563. this.getOptions()
  564. this.fetchData()
  565. this.onProjectSearchDebounced = debounce(() => {
  566. this.projectPageNum = 1
  567. this.projects = [{ id: '', name: '全部' }]
  568. this.projectHasMore = true
  569. this.fetchProjectList()
  570. }, 300)
  571. },
  572. mounted() {
  573. this.$nextTick(() => {
  574. const el = this.$refs.projectList
  575. if (!el) return
  576. this._onProjectListScroll = debounce(() => {
  577. if (!this.projectHasMore || this.projectLoading) return
  578. const { scrollHeight, scrollTop, clientHeight } = el
  579. if (scrollHeight - scrollTop - clientHeight <= 80) {
  580. this.projectPageNum++
  581. this.fetchProjectList()
  582. }
  583. }, 200)
  584. el.addEventListener('scroll', this._onProjectListScroll)
  585. })
  586. },
  587. beforeDestroy() {
  588. if (this._onProjectListScroll) {
  589. this._onProjectListScroll.cancel()
  590. const el = this.$refs.projectList
  591. if (el) el.removeEventListener('scroll', this._onProjectListScroll)
  592. }
  593. if (this.onProjectSearchDebounced && this.onProjectSearchDebounced.cancel) {
  594. this.onProjectSearchDebounced.cancel()
  595. }
  596. },
  597. methods: {
  598. getTagType,
  599. getDeliveryEventStatusTagType(status) {
  600. return getTagType(deliveryEventStatusTagTypes, status, 'info')
  601. },
  602. parseTime,
  603. sanitizeHtml,
  604. getOptions() {
  605. dictApi
  606. .getDictDataByTypes([
  607. 'sys_product_line',
  608. 'delivery_project_status',
  609. 'delivery_event_type',
  610. 'delivery_event_status',
  611. 'delivery_event_result',
  612. 'feedback_source',
  613. 'sys_yes_no',
  614. ])
  615. .then((res) => {
  616. const dicts = res.data || {}
  617. this.productLineDict = (dicts.sys_product_line && dicts.sys_product_line.values) || []
  618. this.projectStatusOptions = (dicts.delivery_project_status && dicts.delivery_project_status.values) || []
  619. this.deliveryEventTypeOptions = (dicts.delivery_event_type && dicts.delivery_event_type.values) || []
  620. this.deliveryEventStatusOptions = (dicts.delivery_event_status && dicts.delivery_event_status.values) || []
  621. this.deliveryEventResultOptions = (dicts.delivery_event_result && dicts.delivery_event_result.values) || []
  622. this.feedbackSourceOptions = (dicts.feedback_source && dicts.feedback_source.values) || []
  623. this.onSiteOptions = (dicts.sys_yes_no && dicts.sys_yes_no.values) || []
  624. })
  625. .catch((err) => console.log(err))
  626. },
  627. // 获取项目列表(支持滚动分页)
  628. async fetchProjectList() {
  629. if (this.projectLoading) return
  630. this.projectLoading = true
  631. try {
  632. const params = {
  633. pageNum: this.projectPageNum,
  634. pageSize: this.projectPageSize,
  635. productLine: '10,20,30,40,50,60',
  636. sortField: 'contract_no',
  637. sortOrder: 'desc',
  638. attribute9: '10',
  639. }
  640. // 状态筛选
  641. if (this.projectStatusFilter) {
  642. const statusMap = {
  643. pending: '10',
  644. delivering: '20,30,40',
  645. delivered: '50',
  646. }
  647. params.projectStatus = statusMap[this.projectStatusFilter] || this.projectStatusFilter
  648. } else {
  649. params.projectStatus = '10,20,30,40,50'
  650. }
  651. const keyword = this.projectSearch.trim()
  652. if (keyword) {
  653. params.keyWords = keyword
  654. }
  655. const res = await deliveryProjectApi.getDelegatedProjectList(params)
  656. if (res.code === 200 && res.data && res.data.list) {
  657. const projectList = (res.data?.list || []).map((item) => ({
  658. id: String(item.id),
  659. name: item.projectName || item.project_name,
  660. contractNo: item.contractNo || item.contract_no,
  661. productLine: item.productLine || item.product_line,
  662. salesOwner: item.salesUserName || item.sales_user_name,
  663. deliveryOwner: item.deliveryUserName || item.delivery_user_name,
  664. deliveryUserId: item.deliveryUserId || item.delivery_user_id,
  665. status: String(item.projectStatus || item.project_status),
  666. }))
  667. const total = res.data?.total || 0
  668. projectList.sort((a, b) => (b.contractNo || '').localeCompare(a.contractNo || ''))
  669. if (this.projectPageNum === 1) {
  670. this.projects = [{ id: '', name: '全部' }, ...projectList]
  671. } else {
  672. this.projects = this.projects.concat(projectList)
  673. }
  674. this.projectTotal = total
  675. this.projectHasMore = this.projects.length - 1 < total
  676. // 如果选中的项目不在当前列表中,清除选中
  677. if (this.selectedProject && !this.projects.some((project) => project.id === this.selectedProject)) {
  678. this.selectedProject = ''
  679. this.queryForm.projectId = ''
  680. this.queryForm.pageNum = 1
  681. this.fetchEventData()
  682. }
  683. }
  684. } catch (error) {
  685. console.error('获取项目列表失败:', error)
  686. if (this.projectPageNum === 1) {
  687. this.projects = [{ id: '', name: '全部' }]
  688. }
  689. } finally {
  690. this.projectLoading = false
  691. // 内容未填满容器时自动加载下一页
  692. this.$nextTick(() => {
  693. const el = this.$refs.projectList
  694. if (el && this.projectHasMore && !this.projectLoading) {
  695. if (el.scrollHeight <= el.clientHeight) {
  696. this.projectPageNum++
  697. this.fetchProjectList()
  698. }
  699. }
  700. })
  701. }
  702. },
  703. // 获取事件列表数据
  704. async fetchEventData() {
  705. this.loading = true
  706. try {
  707. const params = {
  708. pageNum: this.queryForm.pageNum,
  709. pageSize: this.queryForm.pageSize,
  710. projectId: this.queryForm.projectId ? parseInt(this.queryForm.projectId) : 0,
  711. deliveryEventTitle: this.queryForm.deliveryEventTitle,
  712. deliveryEventDesc: this.queryForm.deliveryEventDesc,
  713. deliveryEventType: this.queryForm.deliveryEventType,
  714. deliveryEventStatus: this.queryForm.deliveryEventStatus,
  715. deliveryEventResult: this.queryForm.deliveryEventResult,
  716. opsUserName: this.queryForm.opsUserName,
  717. feedbackReporter: this.queryForm.feedbackReporter,
  718. sortFields: this.queryForm.sortFields,
  719. }
  720. // 处理反馈时间范围
  721. if (this.queryForm.feedbackDateRange && this.queryForm.feedbackDateRange.length === 2) {
  722. params.feedbackDateStart = this.queryForm.feedbackDateRange[0] + ' 00:00:00'
  723. params.feedbackDateEnd = this.queryForm.feedbackDateRange[1] + ' 23:59:59'
  724. }
  725. // 处理处理时间范围
  726. if (this.queryForm.completeDateRange && this.queryForm.completeDateRange.length === 2) {
  727. params.completeTimeStart = this.queryForm.completeDateRange[0] + ' 00:00:00'
  728. params.completeTimeEnd = this.queryForm.completeDateRange[1] + ' 23:59:59'
  729. }
  730. const res = await deliveryProjectEventApi.getList(params)
  731. if (res.code === 200 && res.data) {
  732. this.tableData = res.data?.list || []
  733. this.total = res.data?.total || 0
  734. } else {
  735. this.tableData = []
  736. this.total = 0
  737. }
  738. } catch (error) {
  739. console.error('获取事件数据失败:', error)
  740. this.tableData = []
  741. this.total = 0
  742. this.$message.error('获取事件数据失败')
  743. } finally {
  744. this.loading = false
  745. }
  746. },
  747. // 获取列表数据(项目+事件)
  748. async fetchData() {
  749. await this.fetchProjectList()
  750. await this.fetchEventData()
  751. },
  752. // 搜索
  753. handleSearch() {
  754. this.queryForm.pageNum = 1
  755. this.fetchEventData()
  756. },
  757. // 重置
  758. handleReset() {
  759. this.queryForm = {
  760. pageNum: 1,
  761. pageSize: 20,
  762. projectId: this.queryForm.projectId,
  763. deliveryEventTitle: '',
  764. deliveryEventDesc: '',
  765. deliveryEventType: [],
  766. deliveryEventStatus: [],
  767. deliveryEventResult: [],
  768. opsUserName: [],
  769. feedbackReporter: '',
  770. feedbackDateRange: [],
  771. completeDateRange: [],
  772. sortFields: [],
  773. }
  774. this.fetchEventData()
  775. },
  776. // 新增
  777. handleAdd() {
  778. this.editData = null
  779. this.editDialogVisible = true
  780. },
  781. // 导出
  782. handleExport() {
  783. this.$message.info('导出功能开发中...')
  784. },
  785. // 编辑
  786. async handleEdit(row) {
  787. // 校验事件状态
  788. const status = row.deliveryEventStatus || row.delivery_event_status
  789. if (String(status) === '30') {
  790. this.$message.warning('该事件已关闭,不允许编辑')
  791. return
  792. }
  793. if (String(status) === '90') {
  794. this.$message.warning('该事件已作废,不允许编辑')
  795. return
  796. }
  797. try {
  798. const res = await deliveryProjectEventApi.getById(row.id)
  799. if (res.code === 200 && res.data) {
  800. const latestStatus = res.data.deliveryEventStatus || res.data.delivery_event_status
  801. if (String(latestStatus) === '30') {
  802. this.$message.warning('该事件已关闭,不允许编辑')
  803. this.fetchEventData()
  804. return
  805. }
  806. if (String(latestStatus) === '90') {
  807. this.$message.warning('该事件已作废,不允许编辑')
  808. this.fetchEventData()
  809. return
  810. }
  811. this.editData = res.data
  812. } else {
  813. this.editData = row
  814. }
  815. } catch (error) {
  816. console.error('获取事件详情失败:', error)
  817. this.editData = row
  818. } finally {
  819. this.editDialogVisible = true
  820. }
  821. },
  822. // 查看详情
  823. handleView(row) {
  824. this.currentRow = row
  825. this.detailMode = 'view'
  826. this.detailVisible = true
  827. },
  828. // 处理事件 - 打开详情弹窗(处理模式,可以编辑)
  829. async handleProcess(row) {
  830. // 校验事件状态
  831. const status = row.deliveryEventStatus || row.delivery_event_status
  832. if (String(status) === '30') {
  833. this.$message.warning('该事件已关闭,无法进行操作')
  834. return
  835. }
  836. if (String(status) === '90') {
  837. this.$message.warning('该事件已作废,无法进行操作')
  838. return
  839. }
  840. // 获取最新事件详情
  841. try {
  842. const res = await deliveryProjectEventApi.getById(row.id)
  843. if (res.code === 200 && res.data) {
  844. const latestStatus = res.data.deliveryEventStatus || res.data.delivery_event_status
  845. if (String(latestStatus) === '30') {
  846. this.$message.warning('该事件已关闭,无法进行操作')
  847. this.fetchEventData()
  848. return
  849. }
  850. if (String(latestStatus) === '90') {
  851. this.$message.warning('该事件已作废,无法进行操作')
  852. this.fetchEventData()
  853. return
  854. }
  855. this.currentRow = res.data
  856. } else {
  857. this.currentRow = row
  858. }
  859. } catch (error) {
  860. console.error('获取事件详情失败:', error)
  861. this.currentRow = row
  862. }
  863. this.detailMode = 'process'
  864. this.showCloseEventDialog = false
  865. this.detailVisible = true
  866. },
  867. // 是否显示进展按钮(特定事件类型)
  868. showProgressButton(row) {
  869. const eventType = String(row.deliveryEventType || row.delivery_event_type)
  870. return ['31', '32', '33', '35', '38', '40', '41'].includes(eventType)
  871. },
  872. // 查看研发任务进展
  873. handleProgress(row) {
  874. this.progressEventRow = row
  875. this.progressDialogVisible = true
  876. },
  877. // 作废事件 - 直接弹出作废事件弹窗
  878. async handleCancelEvent(row) {
  879. // 校验事件状态
  880. const status = row.deliveryEventStatus || row.delivery_event_status
  881. if (String(status) === '30') {
  882. this.$message.warning('该事件已关闭,无法进行操作')
  883. return
  884. }
  885. if (String(status) === '90') {
  886. this.$message.warning('该事件已作废,无法进行操作')
  887. return
  888. }
  889. // 获取最新事件详情
  890. try {
  891. const res = await deliveryProjectEventApi.getById(row.id)
  892. if (res.code === 200 && res.data) {
  893. const latestStatus = res.data.deliveryEventStatus || res.data.delivery_event_status
  894. if (String(latestStatus) === '30') {
  895. this.$message.warning('该事件已关闭,无法进行操作')
  896. this.fetchEventData()
  897. return
  898. }
  899. if (String(latestStatus) === '90') {
  900. this.$message.warning('该事件已作废,无法进行操作')
  901. this.fetchEventData()
  902. return
  903. }
  904. this.currentRow = res.data
  905. } else {
  906. this.currentRow = row
  907. }
  908. } catch (error) {
  909. console.error('获取事件详情失败:', error)
  910. this.currentRow = row
  911. }
  912. // 直接显示作废事件弹窗
  913. this.cancelEventDialogVisible = true
  914. },
  915. // 删除
  916. handleDelete(row) {
  917. this.$confirm('确认删除该记录?', '提示', {
  918. confirmButtonText: '确定',
  919. cancelButtonText: '取消',
  920. type: 'warning',
  921. })
  922. .then(async () => {
  923. try {
  924. const res = await deliveryProjectEventApi.deleteByIds([row.id])
  925. if (res.code === 200) {
  926. this.$message.success('删除成功')
  927. this.fetchEventData()
  928. } else {
  929. this.$message.error(res.msg || '删除失败')
  930. }
  931. } catch (error) {
  932. console.error('删除失败:', error)
  933. this.$message.error('删除失败')
  934. }
  935. })
  936. .catch(() => {})
  937. },
  938. // 行点击
  939. handleRowClick(row) {
  940. this.handleView(row)
  941. },
  942. // 分页
  943. handleSizeChange(size) {
  944. this.queryForm.pageSize = size
  945. this.fetchData()
  946. },
  947. handleCurrentChange(page) {
  948. this.queryForm.pageNum = page
  949. this.fetchData()
  950. },
  951. // 侧边栏
  952. toggleSidebar() {
  953. this.sidebarCollapsed = !this.sidebarCollapsed
  954. },
  955. toggleSidebarFilters() {
  956. this.showSidebarFilters = !this.showSidebarFilters
  957. },
  958. selectProject(projectId, project) {
  959. // 如果点击的是具体项目卡片且状态为待分配(10),且用户有研发总监/研发主管角色,弹出指派弹窗
  960. if (project && String(project.status) === '10' && this.canManageDelivery) {
  961. this.handleAssign(project)
  962. return
  963. }
  964. this.selectedProject = projectId
  965. this.queryForm.projectId = projectId || ''
  966. this.queryForm.pageNum = 1
  967. this.fetchEventData()
  968. },
  969. // 处理项目负责人指派(待指派状态)
  970. handleAssign(project) {
  971. this.assignDialogData = {
  972. projectId: project.id,
  973. projectName: project.name,
  974. isReassign: false,
  975. currentDeliveryUserId: null,
  976. currentDeliveryUserName: null,
  977. }
  978. this.assignDialogVisible = true
  979. },
  980. // 处理项目负责人改派
  981. handleReassign(project) {
  982. this.assignDialogData = {
  983. projectId: project.id,
  984. projectName: project.name,
  985. isReassign: true,
  986. currentDeliveryUserId: project.deliveryUserId,
  987. currentDeliveryUserName: project.deliveryOwner,
  988. }
  989. this.assignDialogVisible = true
  990. },
  991. // 指派/改派成功回调
  992. handleAssignSuccess() {
  993. this.fetchProjectList()
  994. },
  995. // 查看项目信息
  996. handleViewProjectInfo(project) {
  997. if (!project || !project.id) return
  998. this.selectedProjectId = project.id
  999. this.projectInfoDialogVisible = true
  1000. },
  1001. formatEventTime(time) {
  1002. return time ? parseTime(time, '{y}-{m}-{d} {h}:{i}') : '-'
  1003. },
  1004. // 产品线标签
  1005. getProductLineLabel(productLine) {
  1006. const label = this.selectDictLabel(this.productLineDict, productLine)
  1007. return label || productLine || '-'
  1008. },
  1009. // 状态样式
  1010. getStatusType(status) {
  1011. const map = {
  1012. 10: 'info',
  1013. 20: 'primary',
  1014. 30: 'warning',
  1015. 40: 'success',
  1016. 50: 'success',
  1017. 90: 'danger',
  1018. }
  1019. return map[status] || 'info'
  1020. },
  1021. // 是否可以删除
  1022. canDelete(status) {
  1023. return ['10', '90'].includes(status)
  1024. },
  1025. // 刷新事件列表
  1026. handleRefreshEvent() {
  1027. this.fetchEventData()
  1028. },
  1029. // 远程搜索负责人
  1030. async remoteFetchOpsUsers(search) {
  1031. this.opsUsersLoading = true
  1032. try {
  1033. const payload = { deptId: DEVOPS_DEV_DEPT_ID, pageNum: 1, pageSize: 999 }
  1034. if (search) payload.keyWords = search
  1035. const res = await userApi.getList(payload)
  1036. const list = res.data?.list || []
  1037. this.opsUserOptions = list.map((u) => ({
  1038. value: u.userId ?? u.user_id ?? u.id ?? null,
  1039. label: u.nickName ?? u.nick_name ?? u.name ?? '',
  1040. }))
  1041. } catch (error) {
  1042. console.error('获取负责人列表失败:', error)
  1043. this.opsUserOptions = []
  1044. } finally {
  1045. this.opsUsersLoading = false
  1046. }
  1047. },
  1048. handleOpsUserVisibleChange(visible) {
  1049. if (visible && !this.opsUsersLoading && !this.opsUserOptions.length) {
  1050. this.remoteFetchOpsUsers('')
  1051. }
  1052. },
  1053. // 多列排序:点击表头切换 无→升序→降序→无
  1054. toggleSort(field) {
  1055. const idx = this.queryForm.sortFields.findIndex((s) => s.field === field)
  1056. if (idx === -1) {
  1057. if (this.queryForm.sortFields.length >= 3) return
  1058. this.queryForm.sortFields.push({ field, order: 'asc' })
  1059. } else if (this.queryForm.sortFields[idx].order === 'asc') {
  1060. this.queryForm.sortFields[idx].order = 'desc'
  1061. } else {
  1062. this.queryForm.sortFields.splice(idx, 1)
  1063. }
  1064. this.fetchEventData()
  1065. },
  1066. getSortState(field) {
  1067. const sf = this.queryForm.sortFields.find((s) => s.field === field)
  1068. return sf ? sf.order : ''
  1069. },
  1070. getSortPriority(field) {
  1071. const idx = this.queryForm.sortFields.findIndex((s) => s.field === field)
  1072. return idx === -1 ? '' : String(idx + 1)
  1073. },
  1074. renderSortableHeader(label, field) {
  1075. const vm = this
  1076. return function (h) {
  1077. const state = vm.getSortState(field)
  1078. const prio = vm.getSortPriority(field)
  1079. return h('div', { class: 'sortable-header', on: { click: () => vm.toggleSort(field) } }, [
  1080. h('span', { class: 'sortable-header-label' }, label),
  1081. h('span', { class: 'sort-arrows' }, [
  1082. h('i', { class: ['el-icon-caret-top', { 'sort-active': state === 'asc' }] }),
  1083. h('i', { class: ['el-icon-caret-bottom', { 'sort-active': state === 'desc' }] }),
  1084. ]),
  1085. prio ? h('span', { class: 'sort-priority' }, prio) : null,
  1086. ])
  1087. }
  1088. },
  1089. // 项目状态筛选变化
  1090. handleProjectStatusChange() {
  1091. this.queryForm.pageNum = 1
  1092. this.projectPageNum = 1
  1093. this.projects = [{ id: '', name: '全部' }]
  1094. this.projectHasMore = true
  1095. this.fetchProjectList()
  1096. },
  1097. // 作废事件弹窗 - 文件上传相关
  1098. handleCancelEventFileChange(file, fileList) {
  1099. this.cancelEventFileList = fileList
  1100. this.cancelEventUploadFiles = fileList.filter((f) => f.raw).map((f) => f.raw)
  1101. },
  1102. handleCancelEventFileRemove(file, fileList) {
  1103. this.cancelEventFileList = fileList
  1104. this.cancelEventUploadFiles = fileList.filter((f) => f.raw).map((f) => f.raw)
  1105. },
  1106. async uploadCancelEventAttachments() {
  1107. const uploadedFiles = []
  1108. for (const file of this.cancelEventUploadFiles) {
  1109. try {
  1110. const result = await uploadFileToRichtextServer(file)
  1111. uploadedFiles.push({
  1112. fileName: result.name || file.name,
  1113. fileUrl: result.url,
  1114. fileType: file.type || 'application/octet-stream',
  1115. })
  1116. } catch (err) {
  1117. this.$message.error(`文件 ${file.name} 上传失败`)
  1118. console.error(err)
  1119. throw err
  1120. }
  1121. }
  1122. return uploadedFiles
  1123. },
  1124. // 取消作废事件弹窗
  1125. handleCancelEventDialogCancel() {
  1126. this.cancelEventDialogVisible = false
  1127. this.cancelEventForm = {
  1128. cancelReason: '',
  1129. }
  1130. this.cancelEventFileList = []
  1131. this.cancelEventUploadFiles = []
  1132. },
  1133. // 保存作废事件
  1134. async handleSaveCancelEvent() {
  1135. this.$refs.cancelEventForm.validate(async (valid) => {
  1136. if (!valid) return
  1137. this.submitLoading = true
  1138. try {
  1139. // 上传附件
  1140. const uploadedAttachments = await this.uploadCancelEventAttachments()
  1141. // 调用作废接口
  1142. const cancelData = {
  1143. id: this.currentRow.id,
  1144. cancelReason: this.cancelEventForm.cancelReason,
  1145. attachments: uploadedAttachments,
  1146. }
  1147. const cancelRes = await deliveryProjectEventApi.cancel(cancelData)
  1148. if (cancelRes.code === 200) {
  1149. this.$message.success('事件作废成功')
  1150. this.handleCancelEventDialogCancel()
  1151. this.fetchEventData()
  1152. } else {
  1153. this.$message.error(cancelRes.msg || '作废事件失败')
  1154. }
  1155. } catch (error) {
  1156. console.error('作废事件失败:', error)
  1157. this.$message.error('作废事件失败')
  1158. } finally {
  1159. this.submitLoading = false
  1160. }
  1161. })
  1162. },
  1163. },
  1164. }
  1165. </script>
  1166. <style lang="scss" scoped>
  1167. .delivery-project-page {
  1168. box-sizing: border-box;
  1169. display: flex;
  1170. flex-direction: column;
  1171. min-height: 0;
  1172. padding: 4px;
  1173. height: calc(100vh - 122px);
  1174. background: #f5f7fa;
  1175. overflow: hidden;
  1176. }
  1177. .query-form-container {
  1178. margin-bottom: 4px;
  1179. flex-shrink: 0;
  1180. padding: 8px 12px;
  1181. background: #fff;
  1182. border-radius: 6px;
  1183. box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
  1184. }
  1185. .query-form-basic {
  1186. display: flex;
  1187. align-items: flex-start;
  1188. justify-content: space-between;
  1189. gap: 16px;
  1190. .query-form-fields {
  1191. flex: 1;
  1192. min-width: 0;
  1193. }
  1194. .query-form-actions {
  1195. display: flex;
  1196. align-items: center;
  1197. justify-content: flex-end;
  1198. flex-shrink: 0;
  1199. min-height: 32px;
  1200. white-space: nowrap;
  1201. }
  1202. ::v-deep .el-form {
  1203. display: flex;
  1204. flex-wrap: wrap;
  1205. align-items: center;
  1206. row-gap: 2px;
  1207. }
  1208. ::v-deep .el-form-item {
  1209. margin-bottom: 2px;
  1210. margin-right: 20px;
  1211. }
  1212. ::v-deep .el-form-item__label {
  1213. padding-right: 8px;
  1214. font-size: 13px;
  1215. color: #606266;
  1216. }
  1217. ::v-deep .el-divider--vertical {
  1218. margin: 0 12px;
  1219. }
  1220. }
  1221. .query-form-advanced {
  1222. margin-top: 8px;
  1223. padding-top: 8px;
  1224. border-top: 1px dashed #e4e7ed;
  1225. ::v-deep .el-form {
  1226. display: flex;
  1227. flex-wrap: wrap;
  1228. align-items: center;
  1229. row-gap: 2px;
  1230. }
  1231. ::v-deep .el-form-item {
  1232. margin-bottom: 2px;
  1233. margin-right: 20px;
  1234. }
  1235. ::v-deep .el-form-item__label {
  1236. padding-right: 8px;
  1237. font-size: 13px;
  1238. color: #606266;
  1239. }
  1240. }
  1241. .table-container {
  1242. display: flex;
  1243. flex: 1;
  1244. flex-direction: column;
  1245. min-width: 0;
  1246. min-height: 0;
  1247. overflow: hidden;
  1248. background: #fff;
  1249. border-radius: 4px;
  1250. padding: 6px 8px;
  1251. box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
  1252. }
  1253. .table-scroll-wrapper {
  1254. display: flex;
  1255. flex: 1;
  1256. min-width: 0;
  1257. min-height: 0;
  1258. overflow: hidden;
  1259. }
  1260. .delivery-project-table {
  1261. flex: 1;
  1262. min-width: 0;
  1263. }
  1264. .pagination-wrapper {
  1265. margin-top: auto;
  1266. padding-top: 4px;
  1267. display: flex;
  1268. justify-content: flex-end;
  1269. }
  1270. .pagination-wrapper ::v-deep .el-pagination {
  1271. padding: 0;
  1272. line-height: 32px;
  1273. }
  1274. .event-title-text {
  1275. display: inline-block;
  1276. max-width: 100%;
  1277. color: #303133;
  1278. overflow: hidden;
  1279. text-overflow: ellipsis;
  1280. white-space: nowrap;
  1281. }
  1282. .project-sidebar {
  1283. display: flex;
  1284. flex-direction: column;
  1285. width: 280px;
  1286. min-height: 0;
  1287. background: #fff;
  1288. border-radius: 4px;
  1289. padding: 8px;
  1290. flex-shrink: 0;
  1291. box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
  1292. transition: width 0.2s ease, padding 0.2s ease;
  1293. overflow: hidden;
  1294. &.collapsed {
  1295. width: 44px;
  1296. padding: 8px 4px;
  1297. align-items: center;
  1298. }
  1299. }
  1300. .sort-select,
  1301. .search-type-select {
  1302. width: 120px;
  1303. }
  1304. .search-input {
  1305. width: 200px;
  1306. }
  1307. .project-status-filter {
  1308. margin-bottom: 8px;
  1309. ::v-deep .el-radio-group {
  1310. display: flex;
  1311. width: 100%;
  1312. }
  1313. ::v-deep .el-radio-button {
  1314. flex: 1;
  1315. .el-radio-button__inner {
  1316. width: 100%;
  1317. padding: 8px 0;
  1318. font-size: 12px;
  1319. }
  1320. }
  1321. }
  1322. .project-status-tag {
  1323. flex-shrink: 0;
  1324. padding: 2px 8px;
  1325. font-size: 11px;
  1326. color: #fff;
  1327. background: #909399;
  1328. border-radius: 999px;
  1329. white-space: nowrap;
  1330. &--10 {
  1331. background: #909399;
  1332. }
  1333. &--20 {
  1334. background: #409eff;
  1335. }
  1336. &--30 {
  1337. background: #e6a23c;
  1338. }
  1339. &--40 {
  1340. background: #67c23a;
  1341. }
  1342. &--50 {
  1343. background: #1f9d55;
  1344. }
  1345. &--90 {
  1346. background: #f56c6c;
  1347. }
  1348. }
  1349. .main-content {
  1350. display: flex;
  1351. flex: 1;
  1352. gap: 4px;
  1353. min-height: 0;
  1354. overflow: hidden;
  1355. }
  1356. .sidebar-header {
  1357. display: flex;
  1358. align-items: center;
  1359. justify-content: space-between;
  1360. margin-bottom: 6px;
  1361. }
  1362. .sidebar-title-group {
  1363. display: flex;
  1364. align-items: center;
  1365. gap: 6px;
  1366. }
  1367. .sidebar-actions {
  1368. display: flex;
  1369. align-items: center;
  1370. }
  1371. .project-sidebar.collapsed .sidebar-header {
  1372. justify-content: center;
  1373. margin-bottom: 0;
  1374. width: 100%;
  1375. }
  1376. .project-sidebar.collapsed .project-search,
  1377. .project-sidebar.collapsed .project-list {
  1378. display: none;
  1379. }
  1380. .sidebar-title {
  1381. font-size: 14px;
  1382. font-weight: 500;
  1383. color: #303133;
  1384. }
  1385. .sidebar-collapsed-label {
  1386. display: flex;
  1387. flex: 1;
  1388. align-items: center;
  1389. justify-content: flex-start;
  1390. padding-top: 12px;
  1391. writing-mode: vertical-rl;
  1392. text-orientation: upright;
  1393. white-space: nowrap;
  1394. font-size: 14px;
  1395. font-weight: 500;
  1396. color: #606266;
  1397. letter-spacing: 2px;
  1398. user-select: none;
  1399. }
  1400. .collapse-trigger {
  1401. display: inline-flex;
  1402. align-items: center;
  1403. justify-content: center;
  1404. width: 24px;
  1405. height: 24px;
  1406. padding: 0;
  1407. background: #f5f7fa;
  1408. border: none;
  1409. border-radius: 4px;
  1410. cursor: pointer;
  1411. color: #909399;
  1412. transition: all 0.2s;
  1413. }
  1414. .sidebar-action-btn {
  1415. display: inline-flex;
  1416. align-items: center;
  1417. justify-content: center;
  1418. width: 24px;
  1419. height: 24px;
  1420. padding: 0;
  1421. background: #f5f7fa;
  1422. border: none;
  1423. border-radius: 4px;
  1424. cursor: pointer;
  1425. color: #909399;
  1426. transition: all 0.2s;
  1427. &:hover {
  1428. background: #ecf5ff;
  1429. color: #409eff;
  1430. }
  1431. &.active {
  1432. background: #ecf5ff;
  1433. color: #409eff;
  1434. }
  1435. i {
  1436. font-size: 12px;
  1437. line-height: 1;
  1438. }
  1439. }
  1440. .project-search {
  1441. margin-bottom: 6px;
  1442. }
  1443. .project-list {
  1444. flex: 1;
  1445. min-height: 0;
  1446. overflow-y: auto;
  1447. padding-right: 2px;
  1448. }
  1449. .project-list-loading,
  1450. .project-list-end {
  1451. display: flex;
  1452. align-items: center;
  1453. justify-content: center;
  1454. padding: 12px 0;
  1455. font-size: 13px;
  1456. color: #909399;
  1457. }
  1458. .project-item {
  1459. display: flex;
  1460. align-items: center;
  1461. justify-content: space-between;
  1462. padding: 10px 12px;
  1463. cursor: pointer;
  1464. border-radius: 12px;
  1465. transition: all 0.2s;
  1466. border: 1px solid transparent;
  1467. &:hover {
  1468. background: #f8fbff;
  1469. }
  1470. &.active {
  1471. background: linear-gradient(180deg, #eef6ff 0%, #e6f0ff 100%);
  1472. border-color: #bfd9ff;
  1473. box-shadow: 0 8px 18px rgba(64, 158, 255, 0.12);
  1474. }
  1475. }
  1476. .project-item--all {
  1477. margin-bottom: 8px;
  1478. background: linear-gradient(135deg, #f8fbff 0%, #f3f7fd 100%);
  1479. border-color: #e4edf7;
  1480. }
  1481. .project-overview-label {
  1482. font-size: 15px;
  1483. font-weight: 600;
  1484. color: #303133;
  1485. }
  1486. .project-overview-desc {
  1487. font-size: 12px;
  1488. color: #909399;
  1489. margin-top: 2px;
  1490. }
  1491. .project-card {
  1492. position: relative;
  1493. display: flex;
  1494. flex-direction: column;
  1495. justify-content: center;
  1496. min-height: 98px;
  1497. padding: 8px 10px;
  1498. border-radius: 14px;
  1499. border: 1px solid #ebeef5;
  1500. background: linear-gradient(180deg, #ffffff 0%, #fbfcfe 100%);
  1501. box-shadow: 0 8px 18px rgba(15, 23, 42, 0.05);
  1502. cursor: pointer;
  1503. transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease;
  1504. &:not(:last-child) {
  1505. margin-bottom: 8px;
  1506. }
  1507. &:hover {
  1508. transform: translateY(-1px);
  1509. border-color: #d5e7ff;
  1510. box-shadow: 0 12px 22px rgba(64, 158, 255, 0.12);
  1511. }
  1512. &.active {
  1513. border-color: #8bb8ff;
  1514. background: linear-gradient(180deg, #eff6ff 0%, #f7fbff 100%);
  1515. box-shadow: 0 14px 26px rgba(64, 158, 255, 0.16);
  1516. }
  1517. }
  1518. .project-card-top {
  1519. display: flex;
  1520. align-items: center;
  1521. justify-content: space-between;
  1522. gap: 8px;
  1523. margin-bottom: 6px;
  1524. }
  1525. .project-card-tags {
  1526. display: flex;
  1527. align-items: center;
  1528. gap: 6px;
  1529. flex: 1;
  1530. min-width: 0;
  1531. }
  1532. .project-contract {
  1533. max-width: 108px;
  1534. padding: 2px 8px;
  1535. font-size: 11px;
  1536. color: #5f6b7a;
  1537. background: #f2f6fc;
  1538. border-radius: 999px;
  1539. overflow: hidden;
  1540. text-overflow: ellipsis;
  1541. white-space: nowrap;
  1542. &.clickable {
  1543. cursor: pointer;
  1544. transition: all 0.2s;
  1545. &:hover {
  1546. color: #409eff;
  1547. background: #ecf5ff;
  1548. }
  1549. }
  1550. }
  1551. .project-line-tag {
  1552. flex-shrink: 0;
  1553. padding: 2px 8px;
  1554. font-size: 11px;
  1555. color: #409eff;
  1556. background: #ecf5ff;
  1557. border-radius: 999px;
  1558. white-space: nowrap;
  1559. }
  1560. .project-card-title {
  1561. margin-bottom: 8px;
  1562. font-size: 14px;
  1563. font-weight: 600;
  1564. line-height: 1.3;
  1565. color: #303133;
  1566. overflow: hidden;
  1567. text-overflow: ellipsis;
  1568. white-space: nowrap;
  1569. }
  1570. .project-card-meta {
  1571. display: flex;
  1572. align-items: center;
  1573. gap: 6px;
  1574. }
  1575. .project-card-meta-item {
  1576. display: flex;
  1577. align-items: center;
  1578. flex: 1;
  1579. gap: 6px;
  1580. min-width: 0;
  1581. padding: 4px 8px;
  1582. background: #f7f9fc;
  1583. border-radius: 10px;
  1584. }
  1585. .project-card-meta-icon {
  1586. flex-shrink: 0;
  1587. font-size: 14px;
  1588. color: #409eff;
  1589. }
  1590. .project-card-meta-icon--delivery {
  1591. color: #67c23a;
  1592. }
  1593. .reassign-btn {
  1594. flex-shrink: 0;
  1595. font-size: 12px;
  1596. color: #409eff;
  1597. cursor: pointer;
  1598. margin-left: 4px;
  1599. padding: 2px;
  1600. border-radius: 50%;
  1601. transition: all 0.2s;
  1602. &:hover {
  1603. background: #ecf5ff;
  1604. transform: rotate(180deg);
  1605. }
  1606. }
  1607. .project-card-meta-value {
  1608. flex: 1;
  1609. min-width: 0;
  1610. font-size: 13px;
  1611. color: #606266;
  1612. overflow: hidden;
  1613. text-overflow: ellipsis;
  1614. white-space: nowrap;
  1615. }
  1616. .project-card-check {
  1617. position: absolute;
  1618. top: 12px;
  1619. right: 12px;
  1620. font-size: 14px;
  1621. color: #409eff;
  1622. }
  1623. .el-icon-check {
  1624. font-size: 12px;
  1625. color: #409eff;
  1626. }
  1627. ::v-deep .sortable-header {
  1628. display: inline-flex;
  1629. align-items: center;
  1630. gap: 4px;
  1631. cursor: pointer;
  1632. user-select: none;
  1633. white-space: nowrap;
  1634. .sort-arrows {
  1635. display: inline-flex;
  1636. flex-direction: column;
  1637. line-height: 1;
  1638. i {
  1639. font-size: 10px;
  1640. color: #c0c4cc;
  1641. transition: color 0.2s;
  1642. &.sort-active {
  1643. color: #409eff;
  1644. }
  1645. }
  1646. }
  1647. .sort-priority {
  1648. display: inline-flex;
  1649. align-items: center;
  1650. justify-content: center;
  1651. width: 16px;
  1652. height: 16px;
  1653. border-radius: 50%;
  1654. background: #409eff;
  1655. color: #fff;
  1656. font-size: 10px;
  1657. font-weight: 700;
  1658. line-height: 1;
  1659. margin-left: 2px;
  1660. }
  1661. }
  1662. .query-select--owner {
  1663. width: 180px;
  1664. }
  1665. </style>