EvaluationDepartmentFeedback.vue 16 KB

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