oneByOneManagementDeptDetail.vue 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557
  1. <template>
  2. <div class="safety-platform-container">
  3. <header v-if="detailData" class="safety-platform-container__header">
  4. <div class="breadcrumb-title">{{ detailData.problem || '-' }}</div>
  5. <div class="detail-content">
  6. <span>创建人:{{ detailData.creatorName || '-' }}</span>
  7. <span>创建时间:{{ detailData.createdAt || '-' }}</span>
  8. </div>
  9. </header>
  10. <main class="safety-platform-container__main">
  11. <div v-if="detailData" class="audit-content">
  12. <el-alert
  13. v-if="detailData.reviewReason"
  14. type="error"
  15. :title="'不通过原因:' + detailData.reviewReason"
  16. show-icon
  17. class="detail-reject-alert"
  18. />
  19. <h4 class="section-title">
  20. <el-icon class="section-title__icon"><Document /></el-icon>
  21. <span>基本信息</span>
  22. </h4>
  23. <div class="detail-ct detail-ct--table">
  24. <div class="row">
  25. <div class="col">
  26. <div class="label">隐患问题:</div>
  27. <div class="value">{{ detailData.problem || '-' }}</div>
  28. </div>
  29. <div class="col">
  30. <div class="label">状态:</div>
  31. <div class="value">{{ detailData.statusName || '-' }}</div>
  32. </div>
  33. </div>
  34. <div class="row">
  35. <div class="col">
  36. <div class="label">下发数:</div>
  37. <div class="value">{{ detailData.issueCount ?? '-' }}</div>
  38. </div>
  39. <div class="col">
  40. <div class="label">反馈数:</div>
  41. <div class="value">{{ detailData.feedbackCount ?? '-' }}</div>
  42. </div>
  43. </div>
  44. <div class="row">
  45. <div class="col">
  46. <div class="label">计划开始日期:</div>
  47. <div class="value">{{ detailData.planStartDate || '-' }}</div>
  48. </div>
  49. <div class="col">
  50. <div class="label">计划完成日期:</div>
  51. <div class="value">{{ detailData.associationOtTimeLimit || '-' }}</div>
  52. </div>
  53. </div>
  54. </div>
  55. <h4 class="section-title">
  56. <el-icon class="section-title__icon"><Document /></el-icon>
  57. <span>举一反三内容</span>
  58. </h4>
  59. <div class="detail-ct requirement-block">
  60. <div class="value value-text">{{ detailData.associationOneThree || '-' }}</div>
  61. </div>
  62. <h4 class="section-title">
  63. <el-icon class="section-title__icon"><Document /></el-icon>
  64. <span>是否存在问题</span>
  65. </h4>
  66. <div class="detail-ct detail-ct--radio">
  67. <el-radio-group v-model="hasProblem" class="has-problem-radio">
  68. <el-radio :label="false">否</el-radio>
  69. <el-radio :label="true">是</el-radio>
  70. </el-radio-group>
  71. </div>
  72. <h4 class="section-title">
  73. <el-icon class="section-title__icon"><Document /></el-icon>
  74. <span>材料上传</span>
  75. </h4>
  76. <div class="detail-ct detail-ct--table attachment-row">
  77. <div class="row">
  78. <div class="col">
  79. <div class="label">上传附件:</div>
  80. <div class="value value--attachment">
  81. <template v-if="attachmentFileList.length">
  82. <div
  83. class="file-container--div"
  84. v-for="item in attachmentFileList"
  85. :key="item.fileUrl || item.fileName"
  86. >
  87. <img
  88. class="file-container--div__icon"
  89. :src="FILE_TYPE_ICON[item.fileType]"
  90. @click="previewOnline(item.fileUrl, item.fileType)"
  91. />
  92. <span
  93. class="file-container--div__name"
  94. @click="previewOnline(item.fileUrl, item.fileType)"
  95. >
  96. {{ item.fileName }}
  97. </span>
  98. <img
  99. class="file-container--div__download"
  100. :src="DownloadIcon"
  101. @click="downloadFile(item.fileUrl, item.fileName)"
  102. />
  103. </div>
  104. </template>
  105. <span v-else class="empty-text">-</span>
  106. </div>
  107. </div>
  108. </div>
  109. <div v-if="hasProblem" class="row row--upload">
  110. <div class="col col--full">
  111. <div class="label">选择附件:</div>
  112. <div class="value value--attachment">
  113. <UploadFiles
  114. label="选择附件"
  115. :file-list="materialAttachmentList"
  116. @uploadSuccess="handleMaterialUploadSuccess"
  117. />
  118. </div>
  119. </div>
  120. </div>
  121. </div>
  122. <h4 v-if="detailData?.issueRecords?.length" class="section-title">
  123. <el-icon class="section-title__icon"><Document /></el-icon>
  124. <span>我的任务 / 下发记录</span>
  125. </h4>
  126. <el-table v-if="detailData?.issueRecords?.length" :data="detailData.issueRecords" border size="small">
  127. <el-table-column prop="associationOtObligationDeptName" label="责任部门" width="120" />
  128. <el-table-column prop="associationOtObligationDeptUserName" label="责任人" width="100" />
  129. <el-table-column prop="feedbackResult" label="反馈结果" min-width="200" show-overflow-tooltip />
  130. <el-table-column prop="feedbackTime" label="反馈时间" width="160" />
  131. <el-table-column prop="statusName" label="状态" width="100" />
  132. <el-table-column label="操作" width="100" fixed="right">
  133. <template #default="{ row }">
  134. <el-button
  135. v-if="canFeedback(row)"
  136. type="primary"
  137. link
  138. size="small"
  139. @click="openFeedbackDialog(row)"
  140. >
  141. 反馈
  142. </el-button>
  143. </template>
  144. </el-table-column>
  145. </el-table>
  146. </div>
  147. </main>
  148. <footer class="safety-platform-container__footer">
  149. <el-button @click="router.back()">返回</el-button>
  150. <el-button type="primary" @click="handleSubmit">
  151. 提交
  152. </el-button>
  153. </footer>
  154. <!-- 部门反馈弹窗 -->
  155. <el-dialog v-model="showFeedbackDialog" title="部门反馈" width="560px" destroy-on-close @close="resetFeedbackForm">
  156. <el-form ref="feedbackFormRef" :model="feedbackForm" :rules="feedbackRules" label-width="100px">
  157. <el-form-item label="反馈结果" prop="feedbackResult">
  158. <el-input
  159. v-model="feedbackForm.feedbackResult"
  160. type="textarea"
  161. :rows="4"
  162. placeholder="请详细说明完成情况"
  163. />
  164. </el-form-item>
  165. <el-form-item label="反馈时间" prop="feedbackTime">
  166. <el-date-picker
  167. v-model="feedbackForm.feedbackTime"
  168. type="datetime"
  169. value-format="YYYY-MM-DD HH:mm:ss"
  170. placeholder="选填,默认当前时间"
  171. style="width: 100%"
  172. />
  173. </el-form-item>
  174. <el-form-item label="附件URL" prop="attachments">
  175. <el-input
  176. v-model="feedbackForm.attachments"
  177. type="textarea"
  178. :rows="2"
  179. placeholder="选填,多个用逗号分隔"
  180. />
  181. </el-form-item>
  182. </el-form>
  183. <template #footer>
  184. <el-button @click="showFeedbackDialog = false">取消</el-button>
  185. <el-button type="primary" @click="handleFeedbackSubmit">提交反馈</el-button>
  186. </template>
  187. </el-dialog>
  188. <PreviewOnline ref="previewOnlineRef" />
  189. </div>
  190. </template>
  191. <script setup lang="ts">
  192. import { computed, onMounted, ref } from 'vue';
  193. import { useRoute, useRouter } from 'vue-router';
  194. import { ElMessage } from 'element-plus';
  195. import type { FormInstance, FormRules } from 'element-plus';
  196. import { Document } from '@element-plus/icons-vue';
  197. import PreviewOnline from '@/views/disaster/components/PreviewOnline.vue';
  198. import UploadFiles from '@/components/UploadFiles/UploadFiles.vue';
  199. import type { FileItem } from '@/components/UploadFiles/types';
  200. import { formatAttachmentList } from '@/components/UploadFiles/utils';
  201. import { getDrawLessonsAdminDetail } from '@/api/drawLessons';
  202. import { submitDrawLessonsDeptFeedback, type DeptFeedbackRequest } from '@/api/drawLessons';
  203. import { FILE_TYPE_ICON } from '@/components/UploadFiles/constants';
  204. import DownloadIcon from '@/views/disaster/disaster-control/src/svg/download.svg';
  205. import { downloadFile } from '@/views/disaster/utils';
  206. const router = useRouter();
  207. const route = useRoute();
  208. const currentId = computed(() => Number(route.query.id));
  209. /** 是否存在问题:否/是,为是时材料上传显示选择附件 */
  210. const hasProblem = ref(false);
  211. /** 材料上传中「选择附件」已选文件(仅当 是否存在问题=是 时使用,提交反馈时可带入) */
  212. const materialAttachmentList = ref<FileItem[]>([]);
  213. const previewOnlineRef = ref<InstanceType<typeof PreviewOnline>>();
  214. /** 详情数据(主记录 + issueRecords) */
  215. const detailData = ref<{
  216. problem?: string;
  217. creatorName?: string;
  218. createdAt?: string;
  219. associationOneThree?: string;
  220. associationOtTimeLimit?: string;
  221. planStartDate?: string;
  222. issueCount?: number;
  223. feedbackCount?: number;
  224. attachments?: string;
  225. attachmentList?: Array<{ fileName?: string; fileUrl?: string; fileNameOrUrl?: string; url?: string }>;
  226. statusName?: string;
  227. statusId?: number;
  228. /** 不通过原因(审核不通过时由后端返回) */
  229. reviewReason?: string;
  230. issueRecords?: Array<{
  231. id: number;
  232. associationOtObligationDeptName?: string;
  233. associationOtObligationDeptUserName?: string;
  234. feedbackResult?: string;
  235. feedbackTime?: string;
  236. statusName?: string;
  237. statusId?: number;
  238. }>;
  239. } | null>(null);
  240. /** 附件列表(从详情 attachments / attachmentList 解析,无数据时为空),样式与安全考核管理列表「考核文档」一致 */
  241. const attachmentFileList = computed(() => {
  242. const d = detailData.value;
  243. if (!d) return [];
  244. const normalize = (items: Array<{ fileUrl?: string; url?: string; fileName?: string; fileNameOrUrl?: string }>) => {
  245. return items.map((raw, index) => {
  246. const url = raw.fileUrl || raw.url || raw.fileNameOrUrl || '';
  247. const nameFromData = raw.fileName || raw.fileNameOrUrl;
  248. const fileName = nameFromData || (url ? url.split('/').pop() || `附件${index + 1}` : `附件${index + 1}`);
  249. const ext = fileName.split('.').pop()?.toLowerCase() || '';
  250. let fileType: 'pdf' | 'word' | 'excel' | 'ppt' = 'pdf';
  251. if (ext === 'doc' || ext === 'docx') fileType = 'word';
  252. else if (ext === 'xls' || ext === 'xlsx') fileType = 'excel';
  253. else if (ext === 'ppt' || ext === 'pptx') fileType = 'ppt';
  254. return {
  255. fileUrl: url,
  256. fileName,
  257. fileType,
  258. };
  259. });
  260. };
  261. if (Array.isArray(d.attachmentList) && d.attachmentList.length) {
  262. return normalize(d.attachmentList);
  263. }
  264. if (d.attachments) {
  265. try {
  266. const parsed = JSON.parse(d.attachments);
  267. if (Array.isArray(parsed) && parsed.length) {
  268. return normalize(parsed as Array<{ fileUrl?: string; url?: string; fileName?: string; fileNameOrUrl?: string }>);
  269. }
  270. } catch {
  271. const list = d.attachments
  272. .split(',')
  273. .map((s) => s.trim())
  274. .filter(Boolean)
  275. .map((s) => ({ fileNameOrUrl: s, fileUrl: s }));
  276. if (list.length) return normalize(list);
  277. }
  278. }
  279. return [];
  280. });
  281. const previewOnline = (url: string | undefined, type: string) => {
  282. if (url) previewOnlineRef.value?.open(url, type);
  283. };
  284. function handleMaterialUploadSuccess(fileList: FileItem[]) {
  285. materialAttachmentList.value = fileList;
  286. }
  287. /** 下发记录状态:3-待反馈 可反馈;4-待审核 已反馈不可再提交 */
  288. function canFeedback(row: { statusId?: number }) {
  289. return row.statusId === 3;
  290. }
  291. /** 底部提交:直接调用反馈接口(使用当前详情主记录 id) */
  292. async function handleSubmit() {
  293. const id = currentId.value;
  294. if (!id) return;
  295. try {
  296. let attachments = '';
  297. if (hasProblem.value && materialAttachmentList.value.length) {
  298. const formatted = await formatAttachmentList(materialAttachmentList.value);
  299. attachments = JSON.stringify(formatted);
  300. }
  301. const now = new Date();
  302. const feedbackTime = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')} ${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}`;
  303. await submitDrawLessonsDeptFeedback({
  304. id,
  305. feedbackHasIssue: hasProblem.value ? 1 : 0,
  306. feedbackResult: '',
  307. feedbackTime,
  308. attachments,
  309. });
  310. ElMessage.success('反馈提交成功');
  311. getDetail();
  312. } catch (e) {
  313. console.error('反馈提交失败:', e);
  314. ElMessage.error('反馈提交失败,请重试');
  315. }
  316. }
  317. const getDetail = async () => {
  318. if (!currentId.value) return;
  319. try {
  320. const res = await getDrawLessonsAdminDetail(currentId.value);
  321. const data = (res as any)?.data ?? res;
  322. if (data && typeof data === 'object') {
  323. detailData.value = data;
  324. }
  325. } catch (e) {
  326. console.error('获取举一反三详情失败:', e);
  327. ElMessage.error('获取详情失败');
  328. }
  329. };
  330. // ---------- 部门反馈 ----------
  331. const showFeedbackDialog = ref(false);
  332. const feedbackFormRef = ref<FormInstance>();
  333. /** 当前操作的下发记录 ID(举一反三下发表 id) */
  334. const currentIssueId = ref<number>(0);
  335. const feedbackForm = ref<DeptFeedbackRequest>({
  336. id: 0,
  337. feedbackHasIssue: 0,
  338. feedbackResult: '',
  339. feedbackTime: '',
  340. attachments: '',
  341. });
  342. const feedbackRules: FormRules = {
  343. feedbackResult: [{ required: true, message: '请输入反馈结果', trigger: 'blur' }],
  344. };
  345. function resetFeedbackForm() {
  346. feedbackForm.value = {
  347. id: currentIssueId.value,
  348. feedbackHasIssue: hasProblem.value ? 1 : 0,
  349. feedbackResult: '',
  350. feedbackTime: '',
  351. attachments: '',
  352. };
  353. }
  354. async function openFeedbackDialog(row: { id: number }) {
  355. currentIssueId.value = row.id;
  356. resetFeedbackForm();
  357. // 若「是否存在问题」为是且已选择附件,带入反馈表单
  358. if (hasProblem.value && materialAttachmentList.value.length) {
  359. try {
  360. const formatted = await formatAttachmentList(materialAttachmentList.value);
  361. feedbackForm.value.attachments = JSON.stringify(formatted);
  362. } catch (e) {
  363. console.error('附件格式化失败:', e);
  364. }
  365. }
  366. showFeedbackDialog.value = true;
  367. }
  368. async function handleFeedbackSubmit() {
  369. await feedbackFormRef.value?.validate?.().catch(() => {});
  370. try {
  371. feedbackForm.value.id = currentIssueId.value;
  372. feedbackForm.value.feedbackHasIssue = hasProblem.value ? 1 : 0;
  373. // 若有「选择附件」且未在 openFeedbackDialog 中写入,这里再带一次
  374. if (hasProblem.value && materialAttachmentList.value.length && !feedbackForm.value.attachments) {
  375. const formatted = await formatAttachmentList(materialAttachmentList.value);
  376. feedbackForm.value.attachments = JSON.stringify(formatted);
  377. }
  378. await submitDrawLessonsDeptFeedback(feedbackForm.value);
  379. ElMessage.success('反馈提交成功');
  380. showFeedbackDialog.value = false;
  381. router.push({
  382. path: '/production-safety/hidden-trouble-investigation-and-governance/one-by-one-management-dept',
  383. });
  384. } catch (e) {
  385. console.error('反馈提交失败:', e);
  386. ElMessage.error('反馈提交失败,请重试');
  387. }
  388. }
  389. onMounted(() => {
  390. getDetail();
  391. });
  392. </script>
  393. <style scoped lang="scss">
  394. @use '@/styles/page-details-layout.scss' as *;
  395. @use '@/styles/page-main-layout.scss' as *;
  396. @use '@/styles/basic-table-file.scss' as *;
  397. .detail-content {
  398. display: flex;
  399. gap: 30px;
  400. margin: 10px 0;
  401. font-size: 14px;
  402. }
  403. .detail-reject-alert {
  404. margin-bottom: 16px;
  405. }
  406. .audit-content {
  407. padding: 0 16px;
  408. .section-title {
  409. display: flex;
  410. align-items: center;
  411. gap: 8px;
  412. margin: 20px 0 12px 0;
  413. font-size: 16px;
  414. font-weight: 600;
  415. color: #333;
  416. .section-title__icon {
  417. font-size: 18px;
  418. color: #333;
  419. }
  420. }
  421. .section-title:first-child {
  422. margin-top: 0;
  423. }
  424. .detail-ct {
  425. font-size: 14px;
  426. margin-bottom: 20px;
  427. &--table {
  428. border: 1px solid #dcdfe6;
  429. .row {
  430. display: flex;
  431. border-bottom: 1px solid #dcdfe6;
  432. &:last-child {
  433. border-bottom: none;
  434. }
  435. }
  436. .col {
  437. display: flex;
  438. flex: 1;
  439. min-height: 40px;
  440. align-items: stretch;
  441. &.col--wide {
  442. flex: 2;
  443. }
  444. &.col--full {
  445. flex: 1 1 100%;
  446. }
  447. .label {
  448. display: flex;
  449. align-items: center;
  450. justify-content: flex-end;
  451. flex-shrink: 0;
  452. width: 140px;
  453. padding: 0 12px;
  454. background-color: #f5f5f5;
  455. border-right: 1px solid #dcdfe6;
  456. color: #333;
  457. }
  458. .value {
  459. flex: 1;
  460. display: flex;
  461. align-items: center;
  462. padding: 10px 20px;
  463. background-color: #fff;
  464. border-right: 1px solid #dcdfe6;
  465. color: #333;
  466. }
  467. }
  468. .row .col:last-child .value {
  469. border-right: none;
  470. }
  471. .row .col:nth-child(2) .label {
  472. border-left: 1px solid #dcdfe6;
  473. }
  474. }
  475. &.requirement-block {
  476. border: 1px solid #e0e0e0;
  477. background-color: #fff;
  478. padding: 16px 20px;
  479. min-height: 60px;
  480. .value-text {
  481. white-space: pre-wrap;
  482. word-break: break-word;
  483. color: #333;
  484. line-height: 1.5;
  485. }
  486. }
  487. &--radio {
  488. border: 1px solid #e0e0e0;
  489. background-color: #fff;
  490. padding: 16px 20px;
  491. .has-problem-radio {
  492. display: flex;
  493. gap: 24px;
  494. }
  495. }
  496. &.attachment-row {
  497. .value--attachment {
  498. flex-wrap: wrap;
  499. align-items: flex-start;
  500. .empty-text {
  501. color: #999;
  502. }
  503. }
  504. .row--upload .value {
  505. align-items: flex-start;
  506. }
  507. }
  508. }
  509. }
  510. </style>