Няма описание

index.vue 33KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191
  1. <script lang="ts" setup>
  2. import { defineComponent, h, onMounted, ref } from 'vue';
  3. import { useRoute } from 'vue-router';
  4. // @ts-ignore
  5. import { Page, useVbenDrawer, useVbenModal } from '@vben/common-ui';
  6. // @ts-ignore
  7. import { useTabs } from '@vben/hooks';
  8. import { useUserStore } from '@vben/stores';
  9. import dayjs from 'dayjs';
  10. import {
  11. ElButton,
  12. ElDescriptions,
  13. ElDescriptionsItem,
  14. ElMessage,
  15. ElMessageBox,
  16. ElTag,
  17. } from 'element-plus';
  18. import { keyBy } from 'lodash-es';
  19. // @ts-ignore
  20. import {
  21. closeTask,
  22. getTaskDetail,
  23. setPlanTimeAPI,
  24. transferTask,
  25. } from '#/api/schedule';
  26. // @ts-ignore
  27. import { confirmDoneTask, handleLicense } from '#/api/task/confirm';
  28. // @ts-ignore
  29. import AttachmentViewer from '#/components/AttachmentViewer.vue';
  30. // @ts-ignore
  31. import { SelectUsers } from '#/components/selectUsers';
  32. // @ts-ignore: 忽略
  33. // @ts-ignore
  34. import { taskLevelOptions, taskStatusOptions } from '#/utils/dicts';
  35. // @ts-ignore: 应急防护月检
  36. import emergencyInfoPage from '#/views/oilstation/emergency/index.vue';
  37. // @ts-ignore: 灭火器档案
  38. import extinguisherInfoPage from '#/views/oilstation/extinguisher/index.vue';
  39. // @ts-ignore: 过滤器档案
  40. import filterInfoPage from '#/views/oilstation/filter/index.vue';
  41. // @ts-ignore: 急救箱档案
  42. import firstaidkitInfoPage from '#/views/oilstation/firstaidkit/index.vue';
  43. // @ts-ignore: 证照档案
  44. import idPhotoInfoPage from '#/views/oilstation/idphoto/index.vue';
  45. // @ts-ignore
  46. import AdditiveComponent from './components/additive.vue';
  47. // @ts-ignore
  48. import BiddingComponent from './components/bidding.vue';
  49. // @ts-ignore
  50. import CheckComponent from './components/check.vue';
  51. // 引入检查项组件、评论组件和查阅组件
  52. // @ts-ignore
  53. import CommentComponent from './components/comment.vue';
  54. // @ts-ignore
  55. import CompetitionComponent from './components/competition.vue';
  56. // @ts-ignore
  57. import EmergencyComponent from './components/emergency.vue';
  58. // @ts-ignore
  59. import EntranceComponent from './components/entrance.vue';
  60. // 引入班组日志组件
  61. // @ts-ignore
  62. import GroupLogComponent from './components/group-log.vue';
  63. // @ts-ignore
  64. import GunCheck2Component from './components/gun-check2.vue';
  65. // @ts-ignore
  66. import GunCheckComponent from './components/gun-check.vue';
  67. // @ts-ignore
  68. import JoinEmergencyComponent from './components/join-emergency.vue';
  69. // @ts-ignore
  70. import MeetingComponent from './components/meeting.vue';
  71. // @ts-ignore
  72. import ManagementPlanComponent from './components/plan.vue';
  73. // @ts-ignore
  74. import SalesSurveyComponent from './components/sales-survey.vue';
  75. // @ts-ignore
  76. import TankWaterComponent from './components/tank-water.vue';
  77. // @ts-ignore
  78. import WeeklyEvaluationComponent from './components/weekly-evaluation.vue';
  79. // @ts-ignore
  80. import WeeklyReportComponent from './components/weekly-report.vue';
  81. import { mobileTaskTypes } from './config-data';
  82. import {
  83. AdditiveDrawer,
  84. CompetitionDrawer,
  85. EmergencyWyDrawer,
  86. EntranceDrawer,
  87. GroupLogDrawer,
  88. GunCheck2Drawer,
  89. GunCheckDrawer,
  90. JoinEmergencyDrawer,
  91. ManagementPlanDrawer,
  92. MeetingDrawer,
  93. OperatingPlanDrawer,
  94. SalesSurveyDrawer,
  95. TankWaterDrawer,
  96. UploadDrawer,
  97. WeeklyEvaluationDrawer,
  98. WeeklyReportDrawer,
  99. } from './drawer';
  100. const userStore = useUserStore();
  101. const { refreshTab } = useTabs();
  102. const route = useRoute();
  103. // const router = useRouter();
  104. const taskId = ref(Number(route.params.id));
  105. // 图片预览相关
  106. const previewVisible = ref(false);
  107. const previewImages = ref<string[]>([]);
  108. // 关闭图片预览
  109. const closePreview = () => {
  110. previewVisible.value = false;
  111. };
  112. // 下载文件
  113. const downloadFile = (fileUrl: string, fileName: string) => {
  114. // 创建临时链接
  115. const link = document.createElement('a');
  116. // 设置下载属性,强制所有文件下载
  117. link.download = fileName;
  118. // 使用 rel="noopener noreferrer" 提高安全性
  119. link.rel = 'noopener noreferrer';
  120. // 对于图片等浏览器会直接打开的文件,使用 fetch 方式获取 blob 再下载
  121. fetch(fileUrl)
  122. .then((response) => response.blob())
  123. .then((blob) => {
  124. // 创建 blob URL
  125. const blobUrl = URL.createObjectURL(blob);
  126. link.href = blobUrl;
  127. // 触发下载
  128. document.body.append(link);
  129. link.click();
  130. // 清理
  131. link.remove();
  132. // 释放 blob URL
  133. setTimeout(() => {
  134. URL.revokeObjectURL(blobUrl);
  135. }, 100);
  136. })
  137. .catch((error) => {
  138. console.error('下载失败:', error);
  139. // 如果 fetch 失败,回退到直接下载
  140. link.href = fileUrl;
  141. document.body.append(link);
  142. link.click();
  143. link.remove();
  144. });
  145. };
  146. // 状态标签配置
  147. const statusConfig: any = keyBy(taskStatusOptions, 'value');
  148. // 优先级标签配置
  149. const priorityConfig: any = keyBy(taskLevelOptions, 'value');
  150. // 关闭任务
  151. const handleCloseTask = () => {
  152. // console.log('关闭任务');
  153. // 二次确认并输入关闭原因
  154. ElMessageBox.prompt('请输入关闭原因', '确认关闭', {
  155. confirmButtonText: '确定',
  156. cancelButtonText: '取消',
  157. type: 'warning',
  158. }).then((reason) => {
  159. // 关闭任务
  160. closeTask({
  161. taskId: taskId.value,
  162. cancelReason: reason.value,
  163. }).then(async () => {
  164. ElMessage.success('任务关闭成功');
  165. // 刷新任务详情
  166. // getTaskDetail(taskId.value).then((res) => {
  167. // taskInfo.value = res;
  168. // });
  169. await init();
  170. });
  171. });
  172. };
  173. const getTaskTime = () => {
  174. return `${dayjs(taskInfo.value.startTime).format('MM-DD HH:mm')}~${dayjs(taskInfo.value.endTime).format('MM-DD HH:mm')}`;
  175. };
  176. const executorRef: any = ref(null);
  177. const [ExecutorModal, executorModalApi] = useVbenModal({
  178. title: '选择考试执行人',
  179. width: 1000,
  180. height: 800,
  181. showCancelBtn: false,
  182. onConfirm: async () => {
  183. const users = await executorRef.value.confirmSelection();
  184. if (users?.length) {
  185. if (users.length !== 1) {
  186. ElMessage.error('请选择一个执行人');
  187. return;
  188. }
  189. // 二次确认
  190. const confirm = await ElMessageBox.confirm(
  191. `确认将任务分配给 ${users[0].name} 吗?`,
  192. '确认分配',
  193. {
  194. confirmButtonText: '确定',
  195. cancelButtonText: '取消',
  196. type: 'warning',
  197. },
  198. );
  199. if (confirm !== 'confirm') {
  200. return;
  201. }
  202. transferTask({
  203. id: taskId.value,
  204. executorId: users[0].id,
  205. }).then(() => {
  206. ElMessage.success('任务转交成功');
  207. // 刷新任务详情
  208. // getTaskDetail(taskId.value).then((res) => {
  209. // taskInfo.value = res;
  210. // });
  211. init();
  212. });
  213. executorModalApi.close();
  214. }
  215. },
  216. });
  217. // 转交任务
  218. const handleTransferTask = () => {
  219. // console.log('转交任务');
  220. executorModalApi.open();
  221. };
  222. const taskInfo: any = ref({});
  223. const taskOther: any = ref({
  224. taskTypeName: '',
  225. });
  226. const taskResult: any = ref({
  227. content: '',
  228. images: [],
  229. files: [],
  230. fileNames: [],
  231. });
  232. // const oneSelf = computed(() => {
  233. // return taskInfo.value.executorId === Number(userStore.userInfo?.userId);
  234. // });
  235. const buttons = ref({
  236. deal: false,
  237. transfer: false,
  238. close: false,
  239. });
  240. const init = async () => {
  241. buttons.value = {
  242. deal: false,
  243. transfer: false,
  244. close: false,
  245. };
  246. const res = await getTaskDetail(taskId.value);
  247. taskInfo.value = res;
  248. if (!taskInfo.value.status) {
  249. taskInfo.value.status = 0;
  250. }
  251. if (taskInfo.value.status === 1) {
  252. const userId = Number(userStore.userInfo?.userId);
  253. const isExecutor = taskInfo.value.executorId === userId;
  254. const isCreator = userStore?.userInfo?.username === taskInfo.value.createBy;
  255. const isHeadquarters = userStore?.userInfo?.post?.type === '"headquarters"';
  256. const isMobileTask = mobileTaskTypes.includes(taskInfo.value.formType);
  257. if (isExecutor && !isMobileTask) {
  258. buttons.value.deal = true;
  259. }
  260. if (isCreator || isHeadquarters || isExecutor) {
  261. if (taskInfo.value.executorId !== userId) {
  262. buttons.value.close = true;
  263. }
  264. if (taskInfo.value.taskTemplate?.isMust === 0) {
  265. buttons.value.close = true;
  266. }
  267. if (taskInfo.value.authStatus === 0 && taskInfo.value.isLicensable) {
  268. buttons.value.transfer = true;
  269. }
  270. }
  271. }
  272. taskOther.value.taskTypeName = taskInfo.value?.formTypeName || '-';
  273. if (!taskInfo.value.executorName && taskInfo.value.taskList?.length) {
  274. taskInfo.value.executorName = taskInfo.value.taskList[0].executorName;
  275. }
  276. if (taskInfo.value.formType === 'photo') {
  277. const result = taskInfo.value?.taskList?.[0] || {};
  278. if (result.remark) {
  279. taskResult.value.content = result.remark;
  280. }
  281. if (result?.attachmentUrl?.length) {
  282. taskResult.value.images = result.attachmentUrl;
  283. }
  284. } else if (taskInfo.value.formType === 'upload') {
  285. // attachmentUrl
  286. const result = taskInfo.value?.taskList?.[0] || {};
  287. if (result.remark) {
  288. taskResult.value.content = result.remark;
  289. }
  290. if (result?.attachmentUrl?.length) {
  291. taskResult.value.files = result.attachmentUrl;
  292. taskResult.value.fileNames = result.attachmentName.split(',');
  293. }
  294. } else {
  295. // taskResult.value = taskInfo.value?.taskList?.[0] || {};
  296. }
  297. if (taskInfo.value?.taskTemplate) {
  298. taskOther.value.taskFrequencyName =
  299. taskInfo.value?.taskTemplate?.taskFrequencyName || '-';
  300. }
  301. };
  302. // 动态组件包装器,根据 taskInfo 动态渲染正确的抽屉组件
  303. const DynamicDrawerComponent = defineComponent({
  304. setup() {
  305. return () => {
  306. let Component = null;
  307. if (taskInfo.value.formType === 'upload') {
  308. Component = UploadDrawer;
  309. } else if (taskInfo.value.formType === 'form') {
  310. switch (taskInfo.value.subFormType) {
  311. case 'additive_inventory':
  312. case 'additive_inventory2': {
  313. Component = AdditiveDrawer;
  314. break;
  315. }
  316. case 'competition_price_survey': {
  317. Component = CompetitionDrawer;
  318. break;
  319. }
  320. case 'competition_sales_survey': {
  321. Component = SalesSurveyDrawer;
  322. break;
  323. }
  324. case 'emergency_wy': {
  325. Component = EmergencyWyDrawer;
  326. break;
  327. }
  328. case 'employee_weekly_evaluation': {
  329. Component = WeeklyEvaluationDrawer;
  330. break;
  331. }
  332. case 'entrance_rate_survey': {
  333. Component = EntranceDrawer;
  334. break;
  335. }
  336. case 'filter_maintenance': {
  337. break;
  338. }
  339. case 'group_log': {
  340. Component = GroupLogDrawer;
  341. break;
  342. }
  343. case 'management_plan': {
  344. Component = ManagementPlanDrawer;
  345. break;
  346. }
  347. case 'meeting': {
  348. Component = MeetingDrawer;
  349. break;
  350. }
  351. case 'oil_gun_self_test': {
  352. Component = GunCheckDrawer;
  353. break;
  354. }
  355. case 'oil_gun_self_test_height': {
  356. Component = GunCheck2Drawer;
  357. break;
  358. }
  359. case 'operating_plan': {
  360. Component = OperatingPlanDrawer;
  361. break;
  362. }
  363. case 'operating_weekly_report': {
  364. Component = WeeklyReportDrawer;
  365. break;
  366. }
  367. case 'participate_emergency_wy':
  368. case 'participate_oil_station_meeting': {
  369. Component = JoinEmergencyDrawer;
  370. break;
  371. }
  372. case 'tank_water': {
  373. Component = TankWaterDrawer;
  374. break;
  375. }
  376. default: {
  377. // ElMessage.error('当前任务无法处理');
  378. Component = null;
  379. }
  380. }
  381. }
  382. return Component ? h(Component) : null;
  383. };
  384. },
  385. });
  386. const [DealDrawer, dealDrawerApi] = useVbenDrawer({
  387. connectedComponent: DynamicDrawerComponent,
  388. onConfirm: () => {
  389. // 刷新当前页面
  390. refreshTab();
  391. },
  392. });
  393. const dealAnnualExaminationTask = async () => {
  394. // 二次弹框确认
  395. const confirm = await ElMessageBox.confirm(
  396. `确认完成任务 ${taskInfo.value.taskName} 吗?`,
  397. '确认完成',
  398. {
  399. confirmButtonText: '确定',
  400. cancelButtonText: '取消',
  401. type: 'warning',
  402. },
  403. );
  404. if (confirm !== 'confirm') {
  405. return;
  406. }
  407. try {
  408. await handleLicense(taskInfo.value.id || 0);
  409. ElMessage.success('处理任务成功');
  410. await init();
  411. } catch {
  412. ElMessage.error('处理任务失败');
  413. }
  414. };
  415. const dealTaskEvent = async () => {
  416. // 二次弹框确认
  417. const confirm = await ElMessageBox.confirm(
  418. `确认完成任务 ${taskInfo.value.taskName} 吗?`,
  419. '确认完成',
  420. {
  421. confirmButtonText: '确定',
  422. cancelButtonText: '取消',
  423. type: 'warning',
  424. },
  425. );
  426. if (confirm !== 'confirm') {
  427. return;
  428. }
  429. try {
  430. await confirmDoneTask(taskInfo.value.id || 0);
  431. ElMessage.success('确认完成任务成功');
  432. await init();
  433. } catch {
  434. ElMessage.error('确认完成任务失败');
  435. }
  436. };
  437. /**
  438. * 处理任务
  439. */
  440. const dealEvent = async () => {
  441. let params: any = {};
  442. let isModelPage = false;
  443. switch (taskInfo.value.formType) {
  444. case 'confirm': {
  445. await dealTaskEvent();
  446. return;
  447. }
  448. case 'form': {
  449. if (
  450. ['annualExamination_task', 'replacement_task'].includes(
  451. taskInfo.value.subFormType,
  452. )
  453. ) {
  454. try {
  455. await dealAnnualExaminationTask();
  456. } catch {
  457. ElMessage.error('处理任务失败');
  458. }
  459. return;
  460. }
  461. params = {
  462. taskId: taskInfo.value.taskList?.[0]?.id || '',
  463. taskName: taskInfo.value.taskName,
  464. taskList: taskInfo.value.taskList || [],
  465. remark: taskResult.value.content,
  466. stationId: taskInfo.value.stationId || 0,
  467. subFormType: taskInfo.value.subFormType,
  468. attachmentUrl: taskResult.value.images,
  469. };
  470. if (
  471. [
  472. 'ambulance_check',
  473. 'emergency_protection_monthly_check',
  474. 'filter_maintenance',
  475. 'fire_extinguisher_check',
  476. ].includes(taskInfo.value.subFormType)
  477. ) {
  478. isModelPage = true;
  479. }
  480. break;
  481. }
  482. case 'upload': {
  483. params = {
  484. taskId: taskInfo.value.taskList?.[0]?.id || '',
  485. taskName: taskInfo.value.taskName,
  486. };
  487. break;
  488. }
  489. default: {
  490. return ElMessage.error('当前任务无法处理');
  491. }
  492. }
  493. if (isModelPage) {
  494. pageModalApi.open();
  495. } else {
  496. dealDrawerApi.setData(params).open();
  497. }
  498. };
  499. const [PageModal, pageModalApi] = useVbenModal({
  500. title: taskInfo.value.taskName || '确认任务',
  501. fullscreen: true,
  502. onConfirm: async () => {
  503. // 二次提醒确认是否完成任务
  504. await dealTaskEvent();
  505. pageModalApi.close();
  506. },
  507. });
  508. onMounted(async () => {
  509. await init();
  510. });
  511. const PlannedTimeValue = ref('') as any;
  512. const [PlannedTimeModal, plannedTimeModalApi] = useVbenModal({
  513. title: '修改计划时间',
  514. showCancelBtn: false,
  515. contentClass: 'plannedTimeModalclass',
  516. onConfirm: async () => {
  517. if (
  518. new Date(PlannedTimeValue.value).getTime() <
  519. new Date(taskInfo.value.startTime).getTime() ||
  520. new Date(PlannedTimeValue.value).getTime() >
  521. new Date(taskInfo.value.endTime).getTime()
  522. ) {
  523. ElMessage.error('计划时间必须在任务开始时间和结束时间之间');
  524. return;
  525. }
  526. try {
  527. await setPlanTimeAPI({
  528. taskId: taskId.value,
  529. planTime: PlannedTimeValue.value,
  530. });
  531. ElMessage.success('修改计划时间成功');
  532. await init();
  533. plannedTimeModalApi.close();
  534. } catch {
  535. ElMessage.error('修改计划时间失败');
  536. }
  537. },
  538. });
  539. const handlePlannedTimeClick = () => {
  540. PlannedTimeValue.value = taskInfo.value.displayDate;
  541. plannedTimeModalApi.open();
  542. };
  543. </script>
  544. <template>
  545. <Page title="" :auto-content-height="true">
  546. <template #description>
  547. <!-- 任务头部信息 -->
  548. <div class="flex items-center justify-between">
  549. <div class="flex items-center">
  550. <div
  551. class="task-icon mr-4 flex h-12 w-12 items-center justify-center rounded-full bg-green-500"
  552. >
  553. <img
  554. :src="taskInfo.iconUrl ? taskInfo.iconUrl[0] : '/icon/4.png'"
  555. alt="任务图标"
  556. class="h-6 w-6"
  557. />
  558. </div>
  559. <div>
  560. <h2 class="mb-1 text-xl font-bold">
  561. {{ taskInfo.taskName }}
  562. <span
  563. v-if="mobileTaskTypes.includes(taskInfo.formType)"
  564. class="ml-2 text-sm text-red-500"
  565. >请在手机端钉钉完成
  566. </span>
  567. </h2>
  568. <div class="flex items-center space-x-2">
  569. <ElTag
  570. :type="priorityConfig[taskInfo.level]?.color || 'info'"
  571. size="small"
  572. effect="dark"
  573. >
  574. {{ priorityConfig[taskInfo.level]?.label || taskInfo.level }}
  575. </ElTag>
  576. <ElTag
  577. :type="
  578. statusConfig[`${taskInfo?.status || -1}`]?.color || 'info'
  579. "
  580. size="small"
  581. effect="dark"
  582. >
  583. {{
  584. statusConfig[`${taskInfo?.status || -1}`]?.label ||
  585. taskInfo.status
  586. }}
  587. <!-- {{ taskInfo.status }} -->
  588. </ElTag>
  589. <ElTag
  590. v-if="taskInfo.isLicensable && taskInfo.authStatus === 0"
  591. size="small"
  592. effect="dark"
  593. color="#215ACD"
  594. >
  595. 可授权
  596. </ElTag>
  597. <ElTag
  598. v-if="taskInfo.isLicensable && taskInfo.authStatus === 1"
  599. size="small"
  600. color="#339169"
  601. effect="dark"
  602. >
  603. 已授权
  604. </ElTag>
  605. <ElTag
  606. v-if="taskInfo.isLicensable && taskInfo.authStatus === 2"
  607. size="small"
  608. color="#EB5E12"
  609. effect="dark"
  610. >
  611. 被授权
  612. </ElTag>
  613. </div>
  614. </div>
  615. </div>
  616. <div class="flex items-center space-x-4">
  617. <ElButton v-if="buttons.deal" type="primary" @click="dealEvent">
  618. 处理任务
  619. </ElButton>
  620. <ElButton
  621. type="primary"
  622. v-if="buttons.transfer"
  623. @click="handleTransferTask"
  624. >
  625. 转交任务
  626. </ElButton>
  627. <ElButton v-if="buttons.close" type="danger" @click="handleCloseTask">
  628. 关闭任务
  629. </ElButton>
  630. </div>
  631. </div>
  632. </template>
  633. <!-- <div>
  634. <span style="font-size: 20px; font-weight: 400">运营异常-任务详情</span>
  635. </div> -->
  636. <div class="boxdev">
  637. <div>
  638. <!-- 基础信息组件 -->
  639. <ElCard>
  640. <template #header>
  641. <div class="flex items-center gap-4">
  642. <div
  643. style="width: 4px; height: 12px; background-color: #215acd"
  644. ></div>
  645. <span
  646. class="text-lg font-bold text-gray-800"
  647. style="font-size: 14px; font-weight: 600"
  648. >
  649. 任务信息
  650. </span>
  651. </div>
  652. </template>
  653. <ElDescriptions class="task-info" :column="4">
  654. <ElDescriptionsItem label="场站:">
  655. {{ taskInfo.stationName || '-' }}
  656. </ElDescriptionsItem>
  657. <ElDescriptionsItem label="标准指引:">
  658. {{ taskInfo.standardGuide || '-' }}
  659. </ElDescriptionsItem>
  660. <ElDescriptionsItem label="执行人:">
  661. {{ taskInfo.executorName || '-' }}
  662. </ElDescriptionsItem>
  663. <ElDescriptionsItem label="计划时间:">
  664. <ElButton type="primary" text @click="handlePlannedTimeClick">
  665. {{ taskInfo.displayDate || '-' }}
  666. </ElButton>
  667. </ElDescriptionsItem>
  668. <ElDescriptionsItem label="创建:">
  669. {{ taskInfo.createBy || '' }}
  670. </ElDescriptionsItem>
  671. <ElDescriptionsItem label="任务类型:">
  672. {{ taskInfo.taskTypeName || '-' }}
  673. </ElDescriptionsItem>
  674. <ElDescriptionsItem label="任务负责人:">
  675. {{ taskInfo.taskLeaderName || '-' }}
  676. </ElDescriptionsItem>
  677. <ElDescriptionsItem label="任务频率:">
  678. {{ taskOther.taskFrequencyName || '-' }}
  679. </ElDescriptionsItem>
  680. <!-- <ElDescriptionsItem label="负责人:">
  681. {{ taskInfo.executorName }}
  682. </ElDescriptionsItem> -->
  683. <ElDescriptionsItem label="任务时间:" :span="4">
  684. {{ getTaskTime() || '-' }}
  685. </ElDescriptionsItem>
  686. <ElDescriptionsItem label="任务描述:" :span="4">
  687. <div v-html="taskInfo.description"></div>
  688. </ElDescriptionsItem>
  689. <ElDescriptionsItem
  690. v-if="
  691. taskInfo.descriptionFilesUrl &&
  692. taskInfo.descriptionFilesUrl.length > 0
  693. "
  694. label="详情附件:"
  695. :span="4"
  696. >
  697. <AttachmentViewer :files="taskInfo.descriptionFilesUrl" />
  698. </ElDescriptionsItem>
  699. <ElDescriptionsItem
  700. v-if="taskResult.handleContent || taskInfo.cancelReason"
  701. label="处理情况:"
  702. :span="4"
  703. >
  704. {{ taskResult.handleContent || taskInfo.cancelReason || '' }}
  705. </ElDescriptionsItem>
  706. <ElDescriptionsItem label="处理时间:" :span="4">
  707. {{ taskInfo.completeTime || taskInfo.cancelTime || '-' }}
  708. </ElDescriptionsItem>
  709. <ElDescriptionsItem
  710. v-if="taskResult.files && taskResult.files.length > 0"
  711. label="处理附件:"
  712. :span="4"
  713. >
  714. <div class="attachment-list">
  715. <div
  716. v-for="(file, index) in taskResult.files"
  717. :key="index"
  718. class="attachment-item"
  719. @click="
  720. () =>
  721. downloadFile(
  722. file,
  723. taskResult.fileNames[index] || `附件${+index + 1}`,
  724. )
  725. "
  726. >
  727. <span class="attachment-name">{{
  728. taskResult.fileNames[index] || `附件${+index + 1}`
  729. }}</span>
  730. </div>
  731. </div>
  732. </ElDescriptionsItem>
  733. </ElDescriptions>
  734. </ElCard>
  735. <!--竞价-->
  736. <ElCard v-if="taskInfo.formType === 'bidding'">
  737. <template #header>
  738. <div class="flex items-center gap-4">
  739. <div
  740. style="width: 4px; height: 12px; background-color: #215acd"
  741. ></div>
  742. <span
  743. class="text-lg font-bold text-gray-800"
  744. style="font-size: 14px; font-weight: 600"
  745. >
  746. 竞价信息
  747. </span>
  748. </div>
  749. </template>
  750. <BiddingComponent />
  751. </ElCard>
  752. <!-- 检查项组件 -->
  753. <ElCard v-if="taskInfo.formType === 'inspection'">
  754. <template #header>
  755. <div class="flex items-center gap-4">
  756. <div
  757. style="width: 4px; height: 12px; background-color: #215acd"
  758. ></div>
  759. <span
  760. class="text-lg font-bold text-gray-800"
  761. style="font-size: 14px; font-weight: 600"
  762. >
  763. 检查项
  764. </span>
  765. </div>
  766. </template>
  767. <CheckComponent :task-detail="taskInfo.taskList[0]" />
  768. </ElCard>
  769. <!-- 班组日志组件 -->
  770. <GroupLogComponent
  771. :task-detail="taskInfo.taskList[0]"
  772. v-if="
  773. taskInfo.formType === 'form' && taskInfo.subFormType === 'group_log'
  774. "
  775. />
  776. <!-- 管理计划组件 -->
  777. <ManagementPlanComponent
  778. :task-detail="taskInfo.taskList[0]"
  779. v-if="
  780. taskInfo.formType === 'form' &&
  781. taskInfo.subFormType === 'management_plan'
  782. "
  783. />
  784. <!-- 销售调查组件 -->
  785. <SalesSurveyComponent
  786. :task-detail="taskInfo.taskList"
  787. v-if="
  788. taskInfo.formType === 'form' &&
  789. taskInfo.subFormType === 'competition_sales_survey'
  790. "
  791. />
  792. <!-- 紧急情况组件 -->
  793. <EmergencyComponent
  794. :task-detail="taskInfo.taskList[0]"
  795. v-if="
  796. taskInfo.formType === 'form' &&
  797. taskInfo.subFormType === 'emergency_wy'
  798. "
  799. />
  800. <!-- 会议组件 -->
  801. <MeetingComponent
  802. :task-detail="taskInfo.taskList[0]"
  803. v-if="
  804. taskInfo.formType === 'form' && taskInfo.subFormType === 'meeting'
  805. "
  806. />
  807. <!-- 入场率调查组件 -->
  808. <EntranceComponent
  809. :task-list="taskInfo.taskList"
  810. v-if="
  811. taskInfo.formType === 'form' &&
  812. taskInfo.subFormType === 'entrance_rate_survey'
  813. "
  814. />
  815. <!-- 竞价结果组件 -->
  816. <CompetitionComponent
  817. :task-detail="taskInfo.taskList"
  818. v-if="
  819. taskInfo.formType === 'form' &&
  820. taskInfo.subFormType === 'competition_price_survey'
  821. "
  822. />
  823. <!-- 周评级组件 -->
  824. <WeeklyEvaluationComponent
  825. :task-detail="taskInfo.taskList"
  826. v-if="
  827. taskInfo.formType === 'form' &&
  828. taskInfo.subFormType === 'employee_weekly_evaluation'
  829. "
  830. />
  831. <!-- 枪试检查组件 -->
  832. <GunCheckComponent
  833. :task-detail="taskInfo.taskList[0]"
  834. v-if="
  835. taskInfo.formType === 'form' &&
  836. taskInfo.subFormType === 'oil_gun_self_test'
  837. "
  838. />
  839. <!-- 枪试检查高度型组件 -->
  840. <GunCheck2Component
  841. :task-detail="taskInfo.taskList[0]"
  842. v-if="
  843. taskInfo.formType === 'form' &&
  844. taskInfo.subFormType === 'oil_gun_self_test_height'
  845. "
  846. />
  847. <!-- 参与紧急情况组件 -->
  848. <JoinEmergencyComponent
  849. :task-detail="{
  850. ...taskInfo.taskList[0],
  851. subFormType: taskInfo.subFormType,
  852. }"
  853. v-if="
  854. taskInfo.formType === 'form' &&
  855. [
  856. 'participate_oil_station_meeting',
  857. 'participate_emergency_wy',
  858. ].includes(taskInfo.subFormType)
  859. "
  860. />
  861. <!-- 量缸试水组件 -->
  862. <TankWaterComponent
  863. :task-detail="taskInfo.taskList[0]"
  864. v-if="
  865. taskInfo.formType === 'form' &&
  866. taskInfo.subFormType === 'tank_water'
  867. "
  868. />
  869. <!-- 处理信息组件 -->
  870. <AdditiveComponent
  871. :task-detail="taskInfo.taskList"
  872. v-if="
  873. taskInfo.formType === 'form' &&
  874. ['additive_inventory', 'additive_inventory2'].includes(
  875. taskInfo.subFormType,
  876. )
  877. "
  878. />
  879. <!-- 周报信息 -->
  880. <WeeklyReportComponent
  881. :task-detail="taskInfo.taskList[0]"
  882. v-if="
  883. taskInfo.formType === 'form' &&
  884. taskInfo.subFormType === 'operating_weekly_report'
  885. "
  886. />
  887. </div>
  888. <div style="width: 412px" v-if="taskInfo.status === 3">
  889. <!-- 评论组件 -->
  890. <ElCard>
  891. <template #header>
  892. <div class="flex items-center gap-4">
  893. <div
  894. style="width: 4px; height: 12px; background-color: #215acd"
  895. ></div>
  896. <span
  897. class="text-lg font-bold text-gray-800"
  898. style="font-size: 14px; font-weight: 600"
  899. >
  900. 评论
  901. </span>
  902. </div>
  903. </template>
  904. <CommentComponent :task-id="taskId" :task-detail="taskInfo" />
  905. </ElCard>
  906. </div>
  907. </div>
  908. <!-- 图片预览组件 -->
  909. <ElImageViewer
  910. v-if="previewVisible"
  911. :url-list="previewImages"
  912. @close="closePreview"
  913. />
  914. <ExecutorModal class="w-[700px]">
  915. <SelectUsers
  916. ref="executorRef"
  917. :params="{
  918. stationId: taskInfo.stationId,
  919. }"
  920. :only-users="true"
  921. :user-params="{
  922. stationId: taskInfo.stationId,
  923. }"
  924. />
  925. </ExecutorModal>
  926. <PlannedTimeModal>
  927. <div>
  928. <span>计划时间:</span>
  929. <el-date-picker
  930. v-model="PlannedTimeValue"
  931. type="datetime"
  932. placeholder="请选择计划时间"
  933. value-format="YYYY-MM-DD HH:mm:ss"
  934. />
  935. </div>
  936. </PlannedTimeModal>
  937. <DealDrawer class="w-3/5" @reload="refreshTab()" />
  938. <PageModal>
  939. <!-- 应急防护月检 -->
  940. <emergencyInfoPage
  941. v-if="taskInfo.subFormType === 'emergency_protection_monthly_check'"
  942. />
  943. <!-- 灭火器档案 -->
  944. <extinguisherInfoPage
  945. v-if="taskInfo.subFormType === 'fire_extinguisher_check'"
  946. />
  947. <!-- 急救箱档案 -->
  948. <firstaidkitInfoPage v-if="taskInfo.subFormType === 'ambulance_check'" />
  949. <!-- 过滤器档案 -->
  950. <filterInfoPage v-if="taskInfo.subFormType === 'filter_maintenance'" />
  951. <!-- 证照档案 -->
  952. <idPhotoInfoPage v-if="taskInfo.subFormType === 'idphoto_check'" />
  953. </PageModal>
  954. </Page>
  955. </template>
  956. <style scoped lang="scss">
  957. .boxdev {
  958. display: flex;
  959. gap: 20px;
  960. height: 100%;
  961. overflow: hidden;
  962. }
  963. .boxdev :deep(.el-card) {
  964. border: none !important;
  965. box-shadow: none !important;
  966. // background: transparent !important;
  967. }
  968. :global(.plannedTimeModalclass) {
  969. min-height: 0px !important;
  970. }
  971. .boxdev :deep(.el-card__header) {
  972. padding-top: 18px !important;
  973. padding-bottom: 4px !important;
  974. padding-left: 0px !important;
  975. border-bottom: none !important;
  976. }
  977. .boxdev :deep(.el-card__body) {
  978. padding-top: 0px !important;
  979. }
  980. ::v-deep .el-descriptions__body {
  981. background: #ffffff00 !important;
  982. }
  983. .boxdev > div:nth-child(1) {
  984. flex: 1;
  985. min-width: 0;
  986. overflow-y: auto;
  987. }
  988. .boxdev > div:nth-child(2) {
  989. width: 28%;
  990. overflow-y: auto;
  991. }
  992. .task-detail-container {
  993. width: 100%;
  994. height: 100%;
  995. }
  996. .task-header {
  997. display: flex;
  998. align-items: center;
  999. justify-content: space-between;
  1000. }
  1001. .task-icon {
  1002. img {
  1003. filter: invert(1);
  1004. }
  1005. }
  1006. .task-info {
  1007. // border: 1px solid red;
  1008. :deep(.el-descriptions__label) {
  1009. font-size: 14px;
  1010. font-weight: 400;
  1011. color: var(--text-color-secondary);
  1012. }
  1013. :deep(.el-descriptions__content) {
  1014. font-size: 14px;
  1015. font-weight: 500;
  1016. color: var(--text-color-primary);
  1017. }
  1018. :deep(.el-descriptions__table) {
  1019. background: transparent !important;
  1020. }
  1021. }
  1022. .task-images {
  1023. display: flex;
  1024. gap: 12px;
  1025. margin-top: 4px;
  1026. .task-image {
  1027. width: 60px;
  1028. height: 60px;
  1029. cursor: pointer;
  1030. object-fit: cover;
  1031. border-radius: 4px;
  1032. transition: transform 0.2s;
  1033. }
  1034. }
  1035. .attachment-list {
  1036. display: flex;
  1037. flex-direction: column;
  1038. gap: 8px;
  1039. margin-top: 8px;
  1040. }
  1041. .attachment-item {
  1042. display: flex;
  1043. align-items: center;
  1044. gap: 12px;
  1045. padding: 12px 16px;
  1046. background-color: #f8f9fa;
  1047. border: 1px solid #e9ecef;
  1048. border-radius: 8px;
  1049. cursor: pointer;
  1050. transition: all 0.3s ease;
  1051. max-width: 100%;
  1052. overflow: hidden;
  1053. text-overflow: ellipsis;
  1054. white-space: nowrap;
  1055. box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
  1056. &:hover {
  1057. background-color: #e7f5ff;
  1058. border-color: #91d5ff;
  1059. color: #409eff;
  1060. box-shadow: 0 2px 8px rgba(64, 158, 255, 0.2);
  1061. transform: translateY(-1px);
  1062. }
  1063. &::before {
  1064. content: '';
  1065. display: inline-block;
  1066. width: 20px;
  1067. height: 20px;
  1068. background-color: #409eff;
  1069. border-radius: 4px;
  1070. background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23ffffff'%3E%3Cpath d='M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z'/%3E%3C/svg%3E");
  1071. background-size: 12px;
  1072. background-position: center;
  1073. background-repeat: no-repeat;
  1074. flex-shrink: 0;
  1075. }
  1076. }
  1077. .attachment-name {
  1078. font-size: 14px;
  1079. font-weight: 500;
  1080. color: #333;
  1081. transition: color 0.3s ease;
  1082. flex: 1;
  1083. overflow: hidden;
  1084. text-overflow: ellipsis;
  1085. white-space: nowrap;
  1086. }
  1087. .attachment-item:hover .attachment-name {
  1088. color: #409eff;
  1089. }
  1090. .attachment-item::after {
  1091. content: '';
  1092. display: inline-block;
  1093. width: 16px;
  1094. height: 16px;
  1095. background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23909399'%3E%3Cpath d='M8.59 16.59L13.17 12 8.59 7.41 10 6l6 6-6 6-1.41-1.41z'/%3E%3C/svg%3E");
  1096. background-size: 14px;
  1097. background-position: center;
  1098. background-repeat: no-repeat;
  1099. flex-shrink: 0;
  1100. transition: transform 0.3s ease;
  1101. }
  1102. .attachment-item:hover::after {
  1103. transform: translateX(4px);
  1104. background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23409eff'%3E%3Cpath d='M8.59 16.59L13.17 12 8.59 7.41 10 6l6 6-6 6-1.41-1.41z'/%3E%3C/svg%3E");
  1105. }
  1106. </style>