oneByOneManagementDeptDetail.vue 19 KB

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