EvaluationDepartmentFeedback.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442
  1. <template>
  2. <main class="safety-platform-container__main">
  3. <!-- 复核不通过提示条:仅在 evaluationDepartment-feedback 且 reviewRejectReson 有值时显示 -->
  4. <el-alert
  5. v-if="isFeedbackOperate && reviewRejectReson"
  6. type="error"
  7. :title="`复核不通过原因:${reviewRejectReson}`"
  8. show-icon
  9. class="reject-alert"
  10. />
  11. <el-form ref="formRef" :model="ruleFormData" :rules="formRules" label-width="auto" class="evaluation-form">
  12. <el-form-item label="考核信息标题:" prop="evaluationTitle">
  13. <el-input v-model="ruleFormData.evaluationTitle" placeholder="请输入考核信息标题" disabled />
  14. </el-form-item>
  15. <el-form-item label="上传附件文档:" prop="attachmentDocument">
  16. <div class="upload-files-disabled">
  17. <UploadFiles label="上传附件" :file-list="ruleFormData.attachmentDocument" @uploadSuccess="handleUploadSuccess" />
  18. </div>
  19. </el-form-item>
  20. <el-form-item label="评分说明:" prop="scoringDescription">
  21. <el-input v-model="ruleFormData.scoringDescription" type="textarea" :rows="5" placeholder="请输入评分说明" disabled />
  22. </el-form-item>
  23. </el-form>
  24. <div class="evaluation-items-section">
  25. <div class="section-header">
  26. <!-- <el-button plain @click="handleDownloadTemplate" disabled>模板下载</el-button>
  27. <el-button plain @click="handleImport">导入</el-button>
  28. <el-button plain @click="handleExport">导出</el-button> -->
  29. <input
  30. ref="importFileInputRef"
  31. type="file"
  32. accept=".xlsx,.xls"
  33. style="display: none"
  34. @change="handleFileChange"
  35. />
  36. </div>
  37. <div class="evaluation-items-table">
  38. <el-table :data="evaluationItems" border show-summary :summary-method="getSummaries">
  39. <el-table-column label="编号" type="index" width="80" align="center" />
  40. <el-table-column label="考核项目" prop="evaluationItem" min-width="150" />
  41. <el-table-column label="考核内容" prop="evaluationContent" min-width="200" />
  42. <el-table-column label="评分方式" prop="scoringMethod" min-width="150" />
  43. <el-table-column label="加减分项" prop="scoreType" min-width="120" />
  44. <el-table-column label="自评得分" prop="selfScore" min-width="180">
  45. <template #default="scope">
  46. <el-input-number
  47. v-model="scope.row.selfScore"
  48. :min="0"
  49. :max="99999"
  50. :precision="0"
  51. :step="1"
  52. placeholder="请输入自评得分"
  53. @blur="handleScoreBlur"
  54. />
  55. </template>
  56. </el-table-column>
  57. <el-table-column label="资料说明" prop="materialDescription" min-width="400">
  58. <template #default="scope">
  59. <UploadFiles
  60. label="上传附件"
  61. :file-list="scope.row.attachmentFileList"
  62. @uploadSuccess="(files: FileItem[]) => handleRowUploadSuccess(scope.$index, files)"
  63. />
  64. </template>
  65. </el-table-column>
  66. <el-table-column label="复核不通过原因" prop="reviewRejectReson" min-width="220"></el-table-column>
  67. </el-table>
  68. </div>
  69. </div>
  70. </main>
  71. <footer class="safety-platform-container__footer">
  72. <el-button @click="router.back()">取消</el-button>
  73. <el-button type="primary" @click="handleSubmit">提交</el-button>
  74. </footer>
  75. </template>
  76. <script setup lang="ts">
  77. import { computed, onMounted, ref } from 'vue';
  78. import { useRouter, useRoute } from 'vue-router';
  79. import { ElMessage } from 'element-plus';
  80. import type { FormInstance } from 'element-plus';
  81. import UploadFiles from '@/components/UploadFiles/UploadFiles.vue';
  82. import {
  83. querySecurityExamineIssueDeptDetail,
  84. updateSecurityExamineDeptSubmit,
  85. exportSecurityExamineIssueDeptDetail,
  86. importSecurityExamineIssueDeptDetail,
  87. } from '@/api/evaluationSystem';
  88. import type { FileItem } from '@/components/UploadFiles/types';
  89. import { formatAttachmentList } from '@/components/UploadFiles/utils';
  90. const props = defineProps<{
  91. id: number;
  92. }>();
  93. const router = useRouter();
  94. const route = useRoute();
  95. // 判断是否为 evaluationDepartment-feedback(反馈)
  96. const isFeedbackOperate = computed(() => route.query.operate === 'evaluationDepartment-feedback');
  97. const formRef = ref<FormInstance>();
  98. const ruleFormData = ref({
  99. evaluationTitle: '',
  100. attachmentDocument: [] as FileItem[],
  101. scoringDescription: '',
  102. });
  103. const formRules = {
  104. evaluationTitle: [{ required: true, message: '请输入考核信息标题', trigger: 'blur' }],
  105. attachmentDocument: [{ required: true, message: '请上传附件文档', trigger: 'change' }],
  106. // scoringDescription: [{ required: true, message: '请输入评分说明', trigger: 'blur' }],
  107. };
  108. const evaluationItems = ref<any[]>([]);
  109. // 保存详情原始数据,用于提交
  110. const detailData = ref<any>(null);
  111. // 复核拒绝原因(用于顶部提示条,仅 evaluationDepartment-feedback 时展示)
  112. const reviewRejectReson = computed(() => {
  113. const val = detailData.value?.reviewRejectReson;
  114. return val && String(val).trim() ? String(val).trim() : '';
  115. });
  116. // 计算总计:根据加减分项计算,加分项加自评得分,减分项减自评得分
  117. const getTotalScore = () => {
  118. return evaluationItems.value.reduce((sum, item) => {
  119. const score = Number(item.selfScore) || 0;
  120. const isAdd = item.isAdd === 1 || item.scoreType === '加分项';
  121. return isAdd ? sum + score : sum - score;
  122. }, 0);
  123. };
  124. // 自评得分失去焦点时触发(用于强制更新合计行)
  125. const handleScoreBlur = () => {
  126. // 触发响应式更新,让合计行重新计算
  127. // 通过修改数组引用来触发更新
  128. evaluationItems.value = [...evaluationItems.value];
  129. };
  130. // 表格合计行方法
  131. const getSummaries = (param: any) => {
  132. const { columns } = param;
  133. const sums: string[] = [];
  134. columns.forEach((column: any, index: number) => {
  135. if (index === 0) {
  136. // 编号列
  137. sums[index] = '';
  138. } else if (column.property === 'scoreType') {
  139. // 加减分项列显示"总计:"
  140. sums[index] = '总计:';
  141. } else if (column.property === 'selfScore') {
  142. // 自评得分列显示总分,即使为0也显示
  143. const total = getTotalScore();
  144. sums[index] = `${total}分`;
  145. } else {
  146. // 其他列显示空
  147. sums[index] = '';
  148. }
  149. });
  150. return sums;
  151. };
  152. const handleValidate = async () => {
  153. if (!formRef.value) return;
  154. return new Promise((resolve) => {
  155. formRef.value?.validate((valid: boolean) => {
  156. resolve(valid);
  157. });
  158. });
  159. };
  160. const handleUploadSuccess = (files: any[]) => {
  161. ruleFormData.value.attachmentDocument = files;
  162. };
  163. const handleDownloadTemplate = () => {
  164. // TODO: 下载模板
  165. console.log('download template');
  166. };
  167. // 导入文件引用
  168. const importFileInputRef = ref<HTMLInputElement>();
  169. const handleImport = () => {
  170. // 触发文件选择
  171. importFileInputRef.value?.click();
  172. };
  173. const handleFileChange = async (event: Event) => {
  174. const target = event.target as HTMLInputElement;
  175. const file = target.files?.[0];
  176. if (!file) return;
  177. try {
  178. await importSecurityExamineIssueDeptDetail({
  179. id: props.id,
  180. file,
  181. });
  182. ElMessage.success('导入成功');
  183. // 重新加载详情数据
  184. await getDetail();
  185. } catch (e: any) {
  186. console.error('导入失败:', e);
  187. ElMessage.error(e?.message || '导入失败,请重试');
  188. } finally {
  189. // 清空文件选择
  190. if (target) {
  191. target.value = '';
  192. }
  193. }
  194. };
  195. const handleExport = async () => {
  196. try {
  197. const blob = await exportSecurityExamineIssueDeptDetail(props.id);
  198. // 创建下载链接
  199. const url = window.URL.createObjectURL(blob);
  200. const link = document.createElement('a');
  201. link.href = url;
  202. link.download = `部门考核详情_${new Date().getTime()}.xlsx`;
  203. document.body.appendChild(link);
  204. link.click();
  205. document.body.removeChild(link);
  206. window.URL.revokeObjectURL(url);
  207. ElMessage.success('导出成功');
  208. } catch (e: any) {
  209. console.error('导出失败:', e);
  210. ElMessage.error(e?.message || '导出失败,请重试');
  211. }
  212. };
  213. // 行内上传附件成功
  214. const handleRowUploadSuccess = (rowIndex: number, files: FileItem[]) => {
  215. if (!evaluationItems.value[rowIndex]) return;
  216. evaluationItems.value[rowIndex].attachmentFileList = files;
  217. };
  218. // 将逗号分隔的 URL 字符串转换为 FileItem[] 格式
  219. const parseAttachmentsToFileList = (attachmentsStr: string | undefined): FileItem[] => {
  220. if (!attachmentsStr || !attachmentsStr.trim()) {
  221. return [];
  222. }
  223. // 按逗号分割URL
  224. const urls = attachmentsStr.split(',').map(url => url.trim()).filter(url => url);
  225. return urls.map((url, index) => {
  226. // 从URL中提取文件名
  227. const urlParts = url.split('/');
  228. const fileName = urlParts[urlParts.length - 1] || `文件${index + 1}`;
  229. // 根据文件扩展名判断文件类型
  230. const extension = fileName.split('.').pop()?.toLowerCase() || '';
  231. let fileType = 'pdf';
  232. if (extension === 'doc' || extension === 'docx') {
  233. fileType = 'word';
  234. } else if (extension === 'xls' || extension === 'xlsx') {
  235. fileType = 'excel';
  236. } else if (extension === 'ppt' || extension === 'pptx') {
  237. fileType = 'ppt';
  238. }
  239. return {
  240. fileId: index + 1,
  241. fileName,
  242. fileType,
  243. fileSize: '0', // 接口未返回文件大小,使用默认值
  244. fileUrl: url,
  245. };
  246. });
  247. };
  248. const getDetail = async () => {
  249. try {
  250. const detail = await querySecurityExamineIssueDeptDetail(props.id);
  251. if (!detail) return;
  252. // 保存原始详情数据,用于提交
  253. detailData.value = detail;
  254. // 映射表单字段
  255. ruleFormData.value.evaluationTitle = detail.exName || ''; // 考核表名称
  256. ruleFormData.value.attachmentDocument = parseAttachmentsToFileList(detail.attachments); // 附件文档
  257. ruleFormData.value.scoringDescription = detail.ratingDescribe; // 评分说明(接口暂无此字段,留空)
  258. // 映射考核项目列表(scores 数组)
  259. if (detail.scores && detail.scores.length > 0) {
  260. evaluationItems.value = detail.scores.map((score) => ({
  261. id: score.id, // 保留评分项ID,用于提交
  262. isAdd: score.isAdd !== undefined ? score.isAdd : (score.selfScore >= 0 ? 1 : 0), // 是否加分项(0-否,1-是)
  263. isAddName: score.isAdd === 1 ? '加分项' : '减分项', // 加减分项名称
  264. scoreType: score.isAdd === 1 ? '加分项' : '减分项', // 加减分项(用于显示,兼容旧字段)
  265. evaluationItem: score.exProgram || '', // 考核项目
  266. evaluationContent: score.exContent || '', // 考核内容
  267. scoringMethod: score.scoringWay || '', // 评分方式
  268. selfScore: score.selfScore || 0, // 自评得分
  269. materialDescription: score.attachments || '', // 资料说明(使用附件字段,字符串)
  270. reviewRejectReson: score.reviewRejectReson || '', // 复核不通过原因
  271. attachmentFileList: parseAttachmentsToFileList(score.attachments || ''), // 对应的附件文件列表
  272. }));
  273. } else {
  274. evaluationItems.value = [];
  275. }
  276. } catch (e) {
  277. console.error('获取部门考核详情失败:', e);
  278. ElMessage.error('获取详情失败,请重试');
  279. }
  280. };
  281. const handleSubmit = async () => {
  282. const res = await handleValidate();
  283. if (!res) return;
  284. try {
  285. if (!detailData.value) {
  286. ElMessage.error('数据加载失败,请刷新后重试');
  287. return;
  288. }
  289. // 使用详情原始数据,更新自评得分、加减分项及资料说明附件
  290. const updatedScores =
  291. (await Promise.all(
  292. (detailData.value.scores || []).map(async (score: any) => {
  293. const item = evaluationItems.value.find((row) => row.id === score.id);
  294. // 处理资料说明附件:将 UploadFiles 返回的文件列表转换为逗号分隔的 URL 字符串
  295. let attachments = score.attachments || '';
  296. if (item && Array.isArray(item.attachmentFileList)) {
  297. const existingFiles: string[] = [];
  298. const newFiles: any[] = [];
  299. item.attachmentFileList.forEach((file: any) => {
  300. if (file.fileUrl && !file.file) {
  301. existingFiles.push(file.fileUrl);
  302. } else {
  303. newFiles.push(file);
  304. }
  305. });
  306. let uploadedUrls: string[] = [];
  307. if (newFiles.length > 0) {
  308. const uploadedFiles = await formatAttachmentList(newFiles);
  309. uploadedUrls = uploadedFiles
  310. .map((f: any) => f.fileUrl || f.url || '')
  311. .filter((url: string) => url);
  312. }
  313. attachments = [...existingFiles, ...uploadedUrls].filter((url) => url).join(',');
  314. }
  315. return {
  316. ...score,
  317. selfScore: item ? Number(item.selfScore) || 0 : score.selfScore || 0,
  318. isAdd: item
  319. ? item.isAdd !== undefined
  320. ? item.isAdd
  321. : item.selfScore >= 0
  322. ? 1
  323. : 0
  324. : score.isAdd !== undefined
  325. ? score.isAdd
  326. : score.selfScore >= 0
  327. ? 1
  328. : 0,
  329. attachments,
  330. };
  331. }),
  332. )) || [];
  333. const submitData = {
  334. ...detailData.value,
  335. scores: updatedScores,
  336. };
  337. await updateSecurityExamineDeptSubmit(submitData);
  338. ElMessage.success('提交成功');
  339. router.back();
  340. } catch (e: any) {
  341. console.error('提交失败:', e);
  342. ElMessage.error(e?.message || '提交失败,请重试');
  343. }
  344. };
  345. onMounted(() => {
  346. getDetail();
  347. });
  348. </script>
  349. <style scoped lang="scss">
  350. @use '@/styles/page-details-layout.scss' as *;
  351. @use '@/styles/basic-table-file.scss' as *;
  352. .reject-alert {
  353. margin-bottom: 20px;
  354. }
  355. .evaluation-form {
  356. display: flex;
  357. flex-direction: column;
  358. width: 600px;
  359. gap: 32px;
  360. :deep(.el-form-item) {
  361. margin-bottom: 0;
  362. }
  363. :deep(.el-form-item__label) {
  364. padding: 0;
  365. }
  366. }
  367. .evaluation-items-section {
  368. margin-top: 32px;
  369. }
  370. .section-header {
  371. display: flex;
  372. gap: 10px;
  373. margin-bottom: 20px;
  374. }
  375. .evaluation-items-table {
  376. width: 100%;
  377. }
  378. .upload-files-disabled {
  379. pointer-events: none;
  380. opacity: 0.6;
  381. :deep(.upload-button) {
  382. cursor: not-allowed;
  383. background-color: #f5f5f5;
  384. color: #aaa;
  385. }
  386. :deep(.delete-button) {
  387. display: none;
  388. }
  389. }
  390. </style>