EvaluationSystemFeedback.vue 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763
  1. <template>
  2. <main class="safety-platform-container__main">
  3. <!-- 反馈审核不通过提示条:仅在 evaluationSystem-feedback 且 approveRejectReson 有值时显示 -->
  4. <el-alert
  5. v-if="isFeedbackOperate && approveRejectReson"
  6. type="error"
  7. :title="approveRejectReson"
  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
  18. label="上传附件"
  19. :file-list="ruleFormData.attachmentDocument"
  20. @uploadSuccess="handleUploadSuccess"
  21. />
  22. </div>
  23. </el-form-item>
  24. <el-form-item label="评分说明:" prop="scoringDescription">
  25. <el-input v-model="ruleFormData.scoringDescription" type="textarea" :rows="5" placeholder="请输入评分说明" disabled />
  26. </el-form-item>
  27. </el-form>
  28. <div class="evaluation-items-section">
  29. <div class="section-header">
  30. <el-button plain @click="handleDownloadTemplate" disabled>模板下载</el-button>
  31. <el-button plain @click="handleImport">导入</el-button>
  32. <el-button plain @click="handleExport">导出</el-button>
  33. <input
  34. ref="importFileInputRef"
  35. type="file"
  36. accept=".xlsx,.xls"
  37. style="display: none"
  38. @change="handleFileChange"
  39. />
  40. </div>
  41. <div class="evaluation-items-table">
  42. <el-table :data="evaluationItems" border show-summary :summary-method="getSummaries">
  43. <el-table-column label="编号" type="index" width="80" align="center" />
  44. <el-table-column label="考核项目" prop="evaluationItem" min-width="150" />
  45. <el-table-column label="考核内容" prop="evaluationContent" min-width="200" />
  46. <el-table-column label="评分方式" prop="scoringMethod" min-width="150" />
  47. <el-table-column label="加减分项" prop="scoreType" min-width="120" />
  48. <el-table-column label="自评得分" prop="selfScore" min-width="120">
  49. <template #default="scope">
  50. {{ scope.row.selfScore }}
  51. </template>
  52. </el-table-column>
  53. <el-table-column label="资料说明" prop="materialDescription" min-width="200">
  54. <template #default="scope">
  55. <div
  56. class="file-container--div"
  57. v-for="item in parseAttachments(scope.row.materialDescription)"
  58. :key="item.fileUrl"
  59. >
  60. <img
  61. class="file-container--div__icon"
  62. @click="previewOnline(item.fileUrl, item.fileType as keyof typeof FILE_TYPE_ICON)"
  63. :src="FILE_TYPE_ICON[item.fileType]"
  64. />
  65. <span
  66. class="file-container--div__name"
  67. @click="previewOnline(item.fileUrl, item.fileType as keyof typeof FILE_TYPE_ICON)"
  68. >{{ item.fileName }}</span
  69. >
  70. <img
  71. class="file-container--div__download"
  72. :src="DownloadIcon"
  73. @click="downloadFile(item.fileUrl, item.fileName)"
  74. />
  75. </div>
  76. </template>
  77. </el-table-column>
  78. <el-table-column label="复核人姓名" prop="reviewUserName" min-width="120" />
  79. <el-table-column label="复核得分" prop="reviewScore" min-width="200">
  80. <template #default="scope">
  81. <el-input-number
  82. v-if="!isAudit && scope.row.isReviewInput"
  83. v-model="scope.row.reviewScore"
  84. :min="0"
  85. :max="99999"
  86. :precision="0"
  87. :step="1"
  88. placeholder="请输入复核得分"
  89. @blur="handleScoreBlur"
  90. />
  91. <span v-else>{{ scope.row.reviewScore }}</span>
  92. </template>
  93. </el-table-column>
  94. <el-table-column label="复核不通过原因" prop="reviewRejectReson" min-width="220">
  95. <template #default="scope">
  96. <el-input
  97. v-if="!isAudit && isSelfApproveButton"
  98. v-model="scope.row.reviewRejectReson"
  99. type="textarea"
  100. :rows="2"
  101. placeholder="请输入复核不通过原因"
  102. />
  103. <span v-else>{{ scope.row.reviewRejectReson}}</span>
  104. </template>
  105. </el-table-column>
  106. </el-table>
  107. </div>
  108. </div>
  109. </main>
  110. <footer class="safety-platform-container__footer">
  111. <el-button @click="router.back()">取消</el-button>
  112. <el-button v-if="!isAudit && isSelfApproveButton" @click="openRejectDialog('review')">复核不通过</el-button>
  113. <el-button v-if="isAudit" @click="openRejectDialog('approve')">审核不通过</el-button>
  114. <el-button v-if="!isAudit && isSelfApproveButton" type="primary" @click="handleSubmitItem">保存</el-button>
  115. <el-button v-if="!isFromDeptView" type="primary" @click="handleSubmit">{{ getSubmitButtonText }}</el-button>
  116. </footer>
  117. <!-- 拒绝原因弹窗 -->
  118. <el-dialog
  119. v-model="rejectDialogVisible"
  120. :title="rejectDialogTitle"
  121. width="600px"
  122. :close-on-click-modal="false"
  123. >
  124. <el-input
  125. v-model="rejectReason"
  126. type="textarea"
  127. :rows="6"
  128. :maxlength="300"
  129. :show-word-limit="true"
  130. :placeholder="rejectDialogPlaceholder"
  131. />
  132. <template #footer>
  133. <el-button @click="closeRejectDialog">取消</el-button>
  134. <el-button type="primary" @click="confirmReject">确定</el-button>
  135. </template>
  136. </el-dialog>
  137. </template>
  138. <script setup lang="ts">
  139. import { computed, ref, watch } from 'vue';
  140. import { useRouter, useRoute } from 'vue-router';
  141. import { ElMessage } from 'element-plus';
  142. import type { FormInstance } from 'element-plus';
  143. import UploadFiles from '@/components/UploadFiles/UploadFiles.vue';
  144. import {
  145. querySecurityExamineIssueDetail,
  146. updateSecurityExamineIssueReviewSubmit,
  147. updateSecurityExamineIssueReviewAgree,
  148. updateSecurityExamineIssueReviewDisagree,
  149. updateSecurityExamineIssueApproveAgree,
  150. updateSecurityExamineIssueApproveDisagree,
  151. exportSecurityExamineIssueDeptDetail,
  152. importSecurityExamineIssueDeptDetail,
  153. } from '@/api/evaluationSystem';
  154. import type { FileItem } from '@/components/UploadFiles/types';
  155. import { formatAttachmentList } from '@/components/UploadFiles/utils';
  156. import DownloadIcon from '@/views/disaster/disaster-control/src/svg/download.svg';
  157. import { downloadFile } from '@/views/disaster/utils';
  158. import PreviewOnline from '@/views/disaster/components/PreviewOnline.vue';
  159. import { FILE_TYPE_ICON } from '@/components/UploadFiles/constants';
  160. const props = defineProps<{
  161. id: number;
  162. }>();
  163. const router = useRouter();
  164. const route = useRoute();
  165. // 判断是评分还是审核
  166. const isAudit = computed(() => route.query.operate === 'evaluationSystem-audit');
  167. // 判断是否为 evaluationSystem-feedback(反馈/评分)
  168. const isFeedbackOperate = computed(() => route.query.operate === 'evaluationSystem-feedback');
  169. // 判断是否从部门考核结果视图进入(如果是,则不显示提交按钮)
  170. const isFromDeptView = computed(() => route.query.fromDeptView === 'true');
  171. // 提交按钮文字
  172. const getSubmitButtonText = computed(() => {
  173. if (isAudit.value) {
  174. return '审核通过';
  175. }
  176. // 如果是评分模式,根据 isSelfApproveButton 决定按钮文字
  177. if (isSelfApproveButton.value) {
  178. return '提交';
  179. }
  180. return '保存';
  181. });
  182. // 拒绝原因弹窗相关
  183. const rejectDialogVisible = ref(false);
  184. const rejectReason = ref('');
  185. const rejectType = ref<'review' | 'approve'>('review'); // 当前拒绝类型
  186. const rejectDialogTitle = computed(() => {
  187. return rejectType.value === 'review'
  188. ? '复核评分不通过的记录,需要填写驳回原因'
  189. : '审核不通过的记录,需要填写驳回原因';
  190. });
  191. const rejectDialogPlaceholder = computed(() => {
  192. return rejectType.value === 'review'
  193. ? '请填写驳回审批原因'
  194. : '请填写驳回审批原因';
  195. });
  196. const openRejectDialog = (type: 'review' | 'approve') => {
  197. rejectType.value = type;
  198. rejectReason.value = '';
  199. rejectDialogVisible.value = true;
  200. };
  201. const closeRejectDialog = () => {
  202. rejectDialogVisible.value = false;
  203. rejectReason.value = '';
  204. };
  205. const formRef = ref<FormInstance>();
  206. const ruleFormData = ref({
  207. evaluationTitle: '',
  208. attachmentDocument: [] as FileItem[],
  209. scoringDescription: '',
  210. });
  211. const formRules = {
  212. evaluationTitle: [{ required: true, message: '请输入考核信息标题', trigger: 'blur' }],
  213. attachmentDocument: [{ required: true, message: '请上传附件文档', trigger: 'change' }],
  214. // scoringDescription: [{ required: true, message: '请输入评分说明', trigger: 'blur' }],
  215. };
  216. const evaluationItems = ref<any[]>([]);
  217. // 保存详情原始数据,用于提交
  218. const detailData = ref<any>(null);
  219. // 是否显示复核不通过按钮(从详情接口获取)
  220. const isSelfApproveButton = ref(false);
  221. // 审批拒绝原因(用于顶部提示条,仅 evaluationSystem-feedback 时展示)
  222. const approveRejectReson = computed(() => {
  223. const val = detailData.value?.approveRejectReson;
  224. return val && String(val).trim() ? String('审核不通过原因:' + val).trim() : '';
  225. });
  226. // 计算自评得分总计:根据加减分项计算,加分项和基础分加、减分项减(基础分按加分项计,与复核得分规则一致)
  227. const getTotalScore = () => {
  228. return evaluationItems.value.reduce((sum, item) => {
  229. const score = Number(item.selfScore) || 0;
  230. const isAdd = item.isAdd === 1 || item.isAdd === 2 || item.scoreType === '加分项' || item.scoreType === '基础分';
  231. return isAdd ? sum + score : sum - score;
  232. }, 0);
  233. };
  234. // 计算复核得分总计:根据加减分项计算,加分项和基础分加、减分项减(基础分按加分项计,与自评得分规则一致)
  235. const getTotalReviewScore = () => {
  236. return evaluationItems.value.reduce((sum, item) => {
  237. const score = Number(item.reviewScore) || 0;
  238. const isAdd = item.isAdd === 1 || item.isAdd === 2 || item.scoreType === '加分项' || item.scoreType === '基础分';
  239. return isAdd ? sum + score : sum - score;
  240. }, 0);
  241. };
  242. // 自评得分失去焦点时触发(用于强制更新合计行)
  243. const handleScoreBlur = () => {
  244. // 触发响应式更新,让合计行重新计算
  245. // 通过修改数组引用来触发更新
  246. evaluationItems.value = [...evaluationItems.value];
  247. };
  248. // 表格合计行方法
  249. const getSummaries = (param: any) => {
  250. const { columns } = param;
  251. const sums: string[] = [];
  252. columns.forEach((column: any, index: number) => {
  253. if (index === 0) {
  254. // 编号列
  255. sums[index] = '';
  256. } else if (column.property === 'scoreType') {
  257. // 加减分项列显示"总计:"
  258. sums[index] = '总计:';
  259. } else if (column.property === 'selfScore') {
  260. // 自评得分列显示总分(按加减分项规则计算)
  261. const total = getTotalScore();
  262. sums[index] = `${total}分`;
  263. } else if (column.property === 'reviewScore') {
  264. // 复核得分列显示总分(按加减分项规则计算,与自评得分一致)
  265. const total = getTotalReviewScore();
  266. sums[index] = `${total}分`;
  267. } else {
  268. // 其他列显示空
  269. sums[index] = '';
  270. }
  271. });
  272. return sums;
  273. };
  274. const handleValidate = async () => {
  275. if (!formRef.value) return;
  276. return new Promise((resolve) => {
  277. formRef.value?.validate((valid: boolean) => {
  278. resolve(valid);
  279. });
  280. });
  281. };
  282. const handleUploadSuccess = (files: any[]) => {
  283. // 评分页顶部附件只读展示,这里仅保持与 UploadFiles 事件兼容
  284. ruleFormData.value.attachmentDocument = files;
  285. };
  286. const handleDownloadTemplate = () => {
  287. // TODO: 下载模板
  288. console.log('download template');
  289. };
  290. // 导入文件引用
  291. const importFileInputRef = ref<HTMLInputElement>();
  292. const handleImport = () => {
  293. // 触发文件选择
  294. importFileInputRef.value?.click();
  295. };
  296. const handleFileChange = async (event: Event) => {
  297. const target = event.target as HTMLInputElement;
  298. const file = target.files?.[0];
  299. if (!file) return;
  300. try {
  301. await importSecurityExamineIssueDeptDetail({
  302. id: props.id,
  303. file,
  304. });
  305. ElMessage.success('导入成功');
  306. // 重新加载详情数据
  307. await getDetail();
  308. } catch (e: any) {
  309. console.error('导入失败:', e);
  310. ElMessage.error(e?.message || '导入失败,请重试');
  311. } finally {
  312. // 清空文件选择
  313. if (target) {
  314. target.value = '';
  315. }
  316. }
  317. };
  318. const handleExport = async () => {
  319. try {
  320. const blob = await exportSecurityExamineIssueDeptDetail(props.id);
  321. // 创建下载链接
  322. const url = window.URL.createObjectURL(blob);
  323. const link = document.createElement('a');
  324. link.href = url;
  325. link.download = `部门考核详情_${new Date().getTime()}.xlsx`;
  326. document.body.appendChild(link);
  327. link.click();
  328. document.body.removeChild(link);
  329. window.URL.revokeObjectURL(url);
  330. ElMessage.success('导出成功');
  331. } catch (e: any) {
  332. console.error('导出失败:', e);
  333. ElMessage.error(e?.message || '导出失败,请重试');
  334. }
  335. };
  336. // 预览
  337. const previewOnlineRef = ref<InstanceType<typeof PreviewOnline>>();
  338. const previewOnline = (url: string | undefined, type: keyof typeof FILE_TYPE_ICON) => {
  339. if (url) {
  340. previewOnlineRef.value?.open(url, type);
  341. }
  342. };
  343. // 解析逗号分隔的URL字符串为文件列表(用于表格资料说明展示)
  344. const parseAttachments = (
  345. attachmentsStr: string | undefined,
  346. ): Array<{
  347. fileUrl: string;
  348. fileName: string;
  349. fileType: string;
  350. }> => {
  351. if (!attachmentsStr || !attachmentsStr.trim()) {
  352. return [];
  353. }
  354. const urls = attachmentsStr
  355. .split(',')
  356. .map((url) => url.trim())
  357. .filter((url) => url);
  358. return urls.map((url) => {
  359. const urlParts = url.split('/');
  360. const fileName = urlParts[urlParts.length - 1] || '未知文件';
  361. const extension = fileName.split('.').pop()?.toLowerCase() || '';
  362. let fileType = 'pdf';
  363. if (extension === 'doc' || extension === 'docx') {
  364. fileType = 'word';
  365. } else if (extension === 'xls' || extension === 'xlsx') {
  366. fileType = 'excel';
  367. } else if (extension === 'ppt' || extension === 'pptx') {
  368. fileType = 'ppt';
  369. }
  370. return {
  371. fileUrl: url,
  372. fileName,
  373. fileType,
  374. };
  375. });
  376. };
  377. // 将逗号分隔的 URL 字符串转换为 FileItem[] 格式
  378. const parseAttachmentsToFileList = (attachmentsStr: string | undefined): FileItem[] => {
  379. if (!attachmentsStr || !attachmentsStr.trim()) {
  380. return [];
  381. }
  382. // 按逗号分割URL
  383. const urls = attachmentsStr.split(',').map(url => url.trim()).filter(url => url);
  384. return urls.map((url, index) => {
  385. // 从URL中提取文件名
  386. const urlParts = url.split('/');
  387. const fileName = urlParts[urlParts.length - 1] || `文件${index + 1}`;
  388. // 根据文件扩展名判断文件类型
  389. const extension = fileName.split('.').pop()?.toLowerCase() || '';
  390. let fileType = 'pdf';
  391. if (extension === 'doc' || extension === 'docx') {
  392. fileType = 'word';
  393. } else if (extension === 'xls' || extension === 'xlsx') {
  394. fileType = 'excel';
  395. } else if (extension === 'ppt' || extension === 'pptx') {
  396. fileType = 'ppt';
  397. }
  398. return {
  399. fileId: index + 1,
  400. fileName,
  401. fileType,
  402. fileSize: '0', // 接口未返回文件大小,使用默认值
  403. fileUrl: url,
  404. };
  405. });
  406. };
  407. const getDetail = async () => {
  408. if (!props.id || isNaN(props.id) || props.id <= 0) {
  409. console.error('无效的ID:', props.id);
  410. ElMessage.error('缺少有效的考核对象ID');
  411. return;
  412. }
  413. try {
  414. console.log('调用详情接口,ID:', props.id);
  415. const detail = await querySecurityExamineIssueDetail(props.id);
  416. if (!detail) {
  417. console.warn('详情接口返回空数据');
  418. return;
  419. }
  420. // 保存原始详情数据,用于提交
  421. detailData.value = detail;
  422. // 获取是否显示复核不通过按钮
  423. isSelfApproveButton.value = detail.isSelfApproveButton === true;
  424. // 映射表单字段
  425. ruleFormData.value.evaluationTitle = detail.exName || ''; // 考核表名称
  426. ruleFormData.value.attachmentDocument = parseAttachmentsToFileList(detail.attachments); // 附件文档
  427. ruleFormData.value.scoringDescription = detail.ratingDescribe; // 评分说明(接口暂无此字段,留空)
  428. // 映射考核项目列表(scores 数组)
  429. if (detail.scores && detail.scores.length > 0) {
  430. evaluationItems.value = detail.scores.map((score) => ({
  431. id: score.id, // 保留评分项ID,用于提交
  432. isAdd: score.isAdd !== undefined ? score.isAdd : (score.selfScore >= 0 ? 1 : 0), // 加减分项(0-减分项,1-加分项,2-基础分)
  433. isAddName: score.isAdd === 2 ? '基础分' : score.isAdd === 1 ? '加分项' : '减分项', // 加减分项名称
  434. scoreType: score.isAdd === 2 ? '基础分' : score.isAdd === 1 ? '加分项' : '减分项', // 加减分项(用于显示,兼容旧字段)
  435. evaluationItem: score.exProgram || '', // 考核项目
  436. evaluationContent: score.exContent || '', // 考核内容
  437. scoringMethod: score.scoringWay || '', // 评分方式
  438. selfScore: score.selfScore, // 自评得分
  439. reviewUserName: score.reviewUserName || '-', // 复核人姓名(从详情顶层获取)
  440. reviewScore: score.reviewScore, // 复核得分
  441. reviewRejectReson: score.reviewRejectReson || '', // 复核不通过原因
  442. materialDescription: score.attachments || '', // 资料说明(使用附件字段,字符串)
  443. attachmentFileList: parseAttachmentsToFileList(score.attachments || ''), // 资料说明对应的附件文件列表
  444. isReviewInput: score.isReviewInput, // 是否显示复核得分输入框
  445. // reviewRejectResonShow: detail.isSelfApproveButton || '', // 复核不通过原因
  446. }));
  447. } else {
  448. evaluationItems.value = [];
  449. }
  450. } catch (e) {
  451. console.error('获取考核对象详情失败:', e);
  452. ElMessage.error('获取详情失败,请重试');
  453. }
  454. };
  455. const handleSubmit = async () => {
  456. const res = await handleValidate();
  457. if (!res) return;
  458. try {
  459. if (!detailData.value) {
  460. ElMessage.error('数据加载失败,请刷新后重试');
  461. return;
  462. }
  463. // 使用详情原始数据,更新复核得分、加减分项及资料说明附件
  464. const updatedScores =
  465. (await Promise.all(
  466. (detailData.value.scores || []).map(async (score: any) => {
  467. const item = evaluationItems.value.find((row) => row.id === score.id);
  468. // 处理资料说明附件:将 UploadFiles 返回的文件列表转换为逗号分隔的 URL 字符串
  469. let attachments = score.attachments || '';
  470. if (item && Array.isArray(item.attachmentFileList)) {
  471. const existingFiles: string[] = [];
  472. const newFiles: any[] = [];
  473. item.attachmentFileList.forEach((file: any) => {
  474. if (file.fileUrl && !file.file) {
  475. existingFiles.push(file.fileUrl);
  476. } else {
  477. newFiles.push(file);
  478. }
  479. });
  480. let uploadedUrls: string[] = [];
  481. if (newFiles.length > 0) {
  482. const uploadedFiles = await formatAttachmentList(newFiles);
  483. uploadedUrls = uploadedFiles
  484. .map((f: any) => f.fileUrl || f.url || '')
  485. .filter((url: string) => url);
  486. }
  487. attachments = [...existingFiles, ...uploadedUrls].filter((url) => url).join(',');
  488. }
  489. return {
  490. ...score,
  491. reviewScore: item ? Number(item.reviewScore) || 0 : score.reviewScore || 0,
  492. reviewRejectReson: item ? item.reviewRejectReson || '' : score.reviewRejectReson || '',
  493. isAdd: item
  494. ? item.isAdd !== undefined
  495. ? item.isAdd
  496. : item.selfScore >= 0
  497. ? 1
  498. : 0
  499. : score.isAdd !== undefined
  500. ? score.isAdd
  501. : score.selfScore >= 0
  502. ? 1
  503. : 0,
  504. attachments,
  505. };
  506. }),
  507. )) || [];
  508. const submitData = {
  509. ...detailData.value,
  510. scores: updatedScores,
  511. };
  512. if (isAudit.value) {
  513. // 审核通过
  514. await updateSecurityExamineIssueApproveAgree(props.id);
  515. ElMessage.success('审核通过操作成功');
  516. } else {
  517. // 评分模式
  518. if (isSelfApproveButton.value) {
  519. // isSelfApproveButton 为 true,调用复核同意接口
  520. await updateSecurityExamineIssueReviewAgree(submitData);
  521. ElMessage.success('提交操作成功');
  522. } else {
  523. // isSelfApproveButton 为 false,调用提交接口
  524. await updateSecurityExamineIssueReviewSubmit(submitData);
  525. ElMessage.success('保存成功');
  526. }
  527. }
  528. router.back();
  529. } catch (e: any) {
  530. console.error('提交失败:', e);
  531. ElMessage.error(e?.message || '提交失败,请重试');
  532. }
  533. };
  534. const handleSubmitItem = async () => {
  535. const res = await handleValidate();
  536. if (!res) return;
  537. try {
  538. if (!detailData.value) {
  539. ElMessage.error('数据加载失败,请刷新后重试');
  540. return;
  541. }
  542. // 使用详情原始数据,更新复核得分、加减分项及资料说明附件
  543. const updatedScores =
  544. (await Promise.all(
  545. (detailData.value.scores || []).map(async (score: any) => {
  546. const item = evaluationItems.value.find((row) => row.id === score.id);
  547. // 处理资料说明附件:将 UploadFiles 返回的文件列表转换为逗号分隔的 URL 字符串
  548. let attachments = score.attachments || '';
  549. if (item && Array.isArray(item.attachmentFileList)) {
  550. const existingFiles: string[] = [];
  551. const newFiles: any[] = [];
  552. item.attachmentFileList.forEach((file: any) => {
  553. if (file.fileUrl && !file.file) {
  554. existingFiles.push(file.fileUrl);
  555. } else {
  556. newFiles.push(file);
  557. }
  558. });
  559. let uploadedUrls: string[] = [];
  560. if (newFiles.length > 0) {
  561. const uploadedFiles = await formatAttachmentList(newFiles);
  562. uploadedUrls = uploadedFiles
  563. .map((f: any) => f.fileUrl || f.url || '')
  564. .filter((url: string) => url);
  565. }
  566. attachments = [...existingFiles, ...uploadedUrls].filter((url) => url).join(',');
  567. }
  568. return {
  569. ...score,
  570. reviewScore: item ? Number(item.reviewScore) || 0 : score.reviewScore || 0,
  571. reviewRejectReson: item ? item.reviewRejectReson || '' : score.reviewRejectReson || '',
  572. isAdd: item
  573. ? item.isAdd !== undefined
  574. ? item.isAdd
  575. : item.selfScore >= 0
  576. ? 1
  577. : 0
  578. : score.isAdd !== undefined
  579. ? score.isAdd
  580. : score.selfScore >= 0
  581. ? 1
  582. : 0,
  583. attachments,
  584. };
  585. }),
  586. )) || [];
  587. const submitData = {
  588. ...detailData.value,
  589. scores: updatedScores,
  590. };
  591. await updateSecurityExamineIssueReviewSubmit(submitData);
  592. ElMessage.success('保存成功');
  593. router.back();
  594. } catch (e: any) {
  595. console.error('提交失败:', e);
  596. ElMessage.error(e?.message || '提交失败,请重试');
  597. }
  598. };
  599. const confirmReject = async () => {
  600. if (!rejectReason.value || !rejectReason.value.trim()) {
  601. ElMessage.warning('请填写驳回原因');
  602. return;
  603. }
  604. try {
  605. if (rejectType.value === 'review') {
  606. const submitData = {
  607. id: props.id,
  608. reviewRejectReson: rejectReason.value.trim(),
  609. scores: evaluationItems.value,
  610. };
  611. await updateSecurityExamineIssueReviewDisagree(submitData);
  612. ElMessage.success('复核不通过操作成功');
  613. } else {
  614. const submitData = {
  615. id: props.id,
  616. approveRejectReson: rejectReason.value.trim(),
  617. psemId: detailData.value.psemId,
  618. };
  619. await updateSecurityExamineIssueApproveDisagree(submitData);
  620. ElMessage.success('审核不通过操作成功');
  621. }
  622. closeRejectDialog();
  623. router.back();
  624. } catch (e: any) {
  625. console.error('操作失败:', e);
  626. ElMessage.error(e?.message || '操作失败,请重试');
  627. }
  628. };
  629. // 监听 props.id 变化,重新加载数据
  630. watch(
  631. () => props.id,
  632. (newId) => {
  633. if (newId && !isNaN(newId) && newId > 0) {
  634. getDetail();
  635. }
  636. },
  637. { immediate: true },
  638. );
  639. </script>
  640. <style scoped lang="scss">
  641. @use '@/styles/page-details-layout.scss' as *;
  642. @use '@/styles/basic-table-file.scss' as *;
  643. .reject-alert {
  644. margin-bottom: 20px;
  645. }
  646. .evaluation-form {
  647. display: flex;
  648. flex-direction: column;
  649. width: 600px;
  650. gap: 32px;
  651. :deep(.el-form-item) {
  652. margin-bottom: 0;
  653. }
  654. :deep(.el-form-item__label) {
  655. padding: 0;
  656. }
  657. }
  658. .evaluation-items-section {
  659. margin-top: 32px;
  660. }
  661. .section-header {
  662. display: flex;
  663. gap: 10px;
  664. margin-bottom: 20px;
  665. }
  666. .evaluation-items-table {
  667. width: 100%;
  668. }
  669. .upload-files-wrapper {
  670. width: 100%;
  671. }
  672. .upload-files-disabled {
  673. pointer-events: none;
  674. opacity: 0.6;
  675. :deep(.upload-button) {
  676. cursor: not-allowed;
  677. background-color: #f5f5f5;
  678. color: #aaa;
  679. }
  680. :deep(.delete-button) {
  681. display: none;
  682. }
  683. }
  684. </style>