oneByOneManagementDeptDetail.vue 17 KB

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