EvaluationSystemDetail.vue 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847
  1. <template>
  2. <main class="safety-platform-container__main">
  3. <el-form
  4. ref="formRef"
  5. :model="ruleFormData"
  6. :rules="formRules"
  7. label-width="auto"
  8. class="evaluation-form"
  9. >
  10. <el-form-item label="考核表名称:" prop="evaluationTableName">
  11. <el-input
  12. v-model="ruleFormData.evaluationTableName"
  13. placeholder="请输入考核表名称"
  14. :disabled="isViewMode"
  15. />
  16. </el-form-item>
  17. <el-form-item label="上传附件文档:" prop="attachmentDocument">
  18. <UploadFiles
  19. label="上传附件"
  20. :file-list="ruleFormData.attachmentDocument"
  21. @uploadSuccess="handleUploadSuccess"
  22. />
  23. </el-form-item>
  24. <el-form-item label="评分说明:" prop="scoringDescription">
  25. <el-input
  26. v-model="ruleFormData.scoringDescription"
  27. type="textarea"
  28. :rows="5"
  29. placeholder="请输入评分说明"
  30. :disabled="isViewMode"
  31. />
  32. </el-form-item>
  33. </el-form>
  34. <div class="evaluation-items-section">
  35. <div class="section-header">
  36. <!-- <el-button plain @click="handleDownloadTemplate">模板下载</el-button> -->
  37. <!-- <el-button plain @click="handleImport">导入</el-button> -->
  38. </div>
  39. <div class="evaluation-items-table">
  40. <el-table :data="evaluationItems" border :span-method="handleSpanMethod">
  41. <el-table-column label="编号" type="index" width="80" align="center" />
  42. <el-table-column label="考核项目" min-width="150">
  43. <template #header>
  44. <span>考核项目<span style="color: red">*</span></span>
  45. </template>
  46. <template #default="scope">
  47. <el-input
  48. :model-value="getCurrentEvaluationItem(scope.$index)"
  49. @update:model-value="(val) => handleEvaluationItemInput(scope.$index, val)"
  50. @blur="handleEvaluationItemBlur(scope.$index)"
  51. placeholder="请输入考核项目"
  52. :disabled="isViewMode"
  53. />
  54. </template>
  55. </el-table-column>
  56. <el-table-column label="考核内容" min-width="200">
  57. <template #header>
  58. <span>考核内容<span style="color: red">*</span></span>
  59. </template>
  60. <template #default="scope">
  61. <el-input
  62. v-model="scope.row.evaluationContent"
  63. placeholder="请输入考核内容"
  64. :disabled="isViewMode"
  65. />
  66. </template>
  67. </el-table-column>
  68. <el-table-column label="评分方式" min-width="150">
  69. <template #default="scope">
  70. <el-input
  71. v-model="scope.row.scoringMethod"
  72. placeholder="请输入评分方式"
  73. :disabled="isViewMode"
  74. />
  75. </template>
  76. </el-table-column>
  77. <el-table-column label="加减分项" min-width="150" align="center">
  78. <template #default="scope">
  79. <el-select
  80. v-model="scope.row.isAdd"
  81. placeholder="请选择"
  82. :disabled="isViewMode"
  83. style="width: 100%"
  84. >
  85. <el-option label="加分项" :value="1" />
  86. <el-option label="减分项" :value="0" />
  87. </el-select>
  88. </template>
  89. </el-table-column>
  90. <el-table-column label="复评人" min-width="150">
  91. <template #default="scope">
  92. <el-select
  93. v-model="scope.row.reviewUserId"
  94. placeholder="请选择复评人"
  95. filterable
  96. clearable
  97. :disabled="isViewMode"
  98. style="width: 100%"
  99. @change="(val) => handleReviewUserChange(scope.$index, val)"
  100. >
  101. <el-option
  102. v-for="user in reviewUserList"
  103. :key="user.id"
  104. :label="user.realname"
  105. :value="user.id"
  106. />
  107. </el-select>
  108. </template>
  109. </el-table-column>
  110. <el-table-column v-if="!isViewMode" label="操作" fixed="right" width="350" align="center">
  111. <template #default="scope">
  112. <el-button type="primary" link @click="handleAddItem(scope.$index)">新增</el-button>
  113. <el-button type="primary" link @click="handleMoveUp(scope.$index)">向上插入分类</el-button>
  114. <el-button type="primary" link @click="handleMoveDown(scope.$index)">向下插入分类</el-button>
  115. <el-button type="danger" link @click="handleDeleteItem(scope.$index)">删除</el-button>
  116. </template>
  117. </el-table-column>
  118. </el-table>
  119. </div>
  120. </div>
  121. <!-- 导入弹窗 -->
  122. <el-dialog v-model="importDialogVisible" title="导入考核项目" width="500px" destroy-on-close>
  123. <el-upload
  124. :file-list="importFileList"
  125. :auto-upload="false"
  126. :on-change="handleFileChange"
  127. :on-remove="handleFileRemove"
  128. :limit="1"
  129. drag
  130. accept=".xlsx,.xls"
  131. >
  132. <el-icon class="el-icon--upload"><Document /></el-icon>
  133. <div class="el-upload__text">
  134. <div style="font-size: 12px; color: red; margin-bottom: 5px">请下载模板并按要求填写后上传</div>
  135. <div style="font-size: 16px">点击或将文件拖拽到这里上传</div>
  136. <div style="font-size: 12px; color: rgba(0, 0, 0, 0.45); margin-top: 5px">
  137. 文件支持.xlsx .xls格式,仅支持上传一个文件
  138. </div>
  139. </div>
  140. </el-upload>
  141. <template #footer>
  142. <span class="dialog-footer">
  143. <el-button @click="importDialogVisible = false">取消</el-button>
  144. <el-button type="primary" @click="handleConfirmImport" :disabled="importFileList.length === 0">
  145. 导入
  146. </el-button>
  147. </span>
  148. </template>
  149. </el-dialog>
  150. </main>
  151. <footer class="safety-platform-container__footer">
  152. <el-button @click="router.back()">取消</el-button>
  153. <el-button v-if="!isViewMode" type="primary" @click="handleSubmit">
  154. {{ isCreateMode ? '提交' : '保存' }}
  155. </el-button>
  156. </footer>
  157. </template>
  158. <script setup lang="ts">
  159. import { computed, nextTick, onMounted, ref } from 'vue';
  160. import { useRoute, useRouter } from 'vue-router';
  161. import { ElMessage } from 'element-plus';
  162. import type { FormInstance } from 'element-plus';
  163. import { Document } from '@element-plus/icons-vue';
  164. import UploadFiles from '@/components/UploadFiles/UploadFiles.vue';
  165. import { EVALUATION_SYSTEM_FORM_DATA, EVALUATION_SYSTEM_FORM_RULES } from '../configs/form';
  166. import { importSecurityExamineDet, saveSecurityExamine, querySecurityExamineDetail, updateSecurityExamine } from '@/api/evaluationSystem';
  167. import type { EvaluationContent } from '@/api/evaluationSystem';
  168. import type { FileItem } from '@/components/UploadFiles/types';
  169. import { useUserInfoHook } from '@/hooks/useUserInfoHook';
  170. import { formatAttachmentList } from '@/components/UploadFiles/utils';
  171. import { queryAvailableUserList } from '@/api/production-safety/responsibility-implementation';
  172. const props = defineProps<{
  173. id?: number;
  174. }>();
  175. const router = useRouter();
  176. const route = useRoute();
  177. const operate = computed(() => (route.query.operate as string) || 'evaluationSystem-create');
  178. const isCreateMode = computed(() => operate.value === 'evaluationSystem-create');
  179. const isEditMode = computed(() => operate.value === 'evaluationSystem-edit');
  180. const isViewMode = computed(() => operate.value === 'evaluationSystem-view');
  181. const formRef = ref<FormInstance>();
  182. const ruleFormData = ref({ ...EVALUATION_SYSTEM_FORM_DATA });
  183. const formRules = EVALUATION_SYSTEM_FORM_RULES;
  184. // 获取当前用户信息
  185. const { id: userId, realname: userName } = useUserInfoHook();
  186. // 初始化一条空白数据
  187. const evaluationItems = ref<any[]>([
  188. {
  189. id: 0,
  190. psemId: 0,
  191. evaluationItem: '',
  192. evaluationContent: '',
  193. scoringMethod: '',
  194. isAdd: 1, // 是否加分项(0-否,1-是),默认为1(加分项)
  195. reviewUserId: null as number | null, // 复评人ID
  196. reviewUserName: '', // 复评人姓名
  197. },
  198. ]);
  199. // 复评人用户列表
  200. const reviewUserList = ref<UserLisItem[]>([]);
  201. const getReviewUserList = async () => {
  202. try {
  203. const res = await queryAvailableUserList({
  204. pageNumber: 1,
  205. pageSize: 9999,
  206. queryParam: {}, // 不传递 deptId 参数
  207. });
  208. reviewUserList.value = res?.records || [];
  209. } catch (e) {
  210. console.error('获取复评人列表失败:', e);
  211. reviewUserList.value = [];
  212. }
  213. };
  214. // 处理复评人选择变化
  215. const handleReviewUserChange = (index: number, userId: number | null) => {
  216. if (userId) {
  217. const selectedUser = reviewUserList.value.find((user) => user.id === userId);
  218. if (selectedUser) {
  219. evaluationItems.value[index].reviewUserName = selectedUser.realname || '';
  220. }
  221. } else {
  222. evaluationItems.value[index].reviewUserName = '';
  223. }
  224. };
  225. // 用于存储输入过程中的临时值,避免实时合并
  226. const tempEvaluationItems = ref<Record<number, string>>({});
  227. const handleValidate = async () => {
  228. if (!formRef.value) return;
  229. return new Promise((resolve) => {
  230. formRef.value?.validate((valid: boolean) => {
  231. resolve(valid);
  232. });
  233. });
  234. };
  235. const handleUploadSuccess = (files: any[]) => {
  236. ruleFormData.value.attachmentDocument = files;
  237. };
  238. const handleDownloadTemplate = () => {
  239. // TODO: 下载模板
  240. console.log('download template');
  241. };
  242. // 导入弹窗相关
  243. const importDialogVisible = ref(false);
  244. const importFileList = ref<any[]>([]);
  245. const handleImport = () => {
  246. importDialogVisible.value = true;
  247. importFileList.value = [];
  248. };
  249. // 处理导入成功
  250. const handleImportSuccess = async (response: any) => {
  251. try {
  252. // 接口返回的数据格式:{ code: 0, message: "string", data: EvaluationContent[] }
  253. if (response && response.data && Array.isArray(response.data)) {
  254. // 将导入的数据映射到 evaluationItems 格式
  255. // 导入的数据视为新增数据,不带后端已保存的 id / psemId
  256. evaluationItems.value = response.data.map((item: any) => ({
  257. id: 0,
  258. psemId: 0,
  259. evaluationItem: item.exProgram || '', // 考核项目
  260. evaluationContent: item.exContent || '', // 考核内容
  261. scoringMethod: item.scoringWay || '', // 评分方式
  262. isAdd: item.isAdd !== undefined ? item.isAdd : 1, // 是否加分项(0-否,1-是),默认为1
  263. reviewUserId: item.reviewUserId || null, // 复评人ID
  264. reviewUserName: item.reviewUserName || '', // 复评人姓名
  265. }));
  266. // 如果导入后列表为空,至少保留一条空白数据
  267. if (evaluationItems.value.length === 0) {
  268. evaluationItems.value = [
  269. {
  270. evaluationItem: '',
  271. evaluationContent: '',
  272. scoringMethod: '',
  273. },
  274. ];
  275. }
  276. ElMessage.success(`导入成功,共导入 ${response.data.length} 条数据`);
  277. importDialogVisible.value = false;
  278. } else {
  279. ElMessage.error('导入失败:返回数据格式错误');
  280. }
  281. } catch (e) {
  282. console.error('导入处理失败:', e);
  283. ElMessage.error('导入处理失败');
  284. }
  285. };
  286. // 处理文件上传
  287. const handleFileChange = (file: any) => {
  288. importFileList.value = [file];
  289. };
  290. // 处理文件移除
  291. const handleFileRemove = () => {
  292. importFileList.value = [];
  293. };
  294. // 确认导入
  295. const handleConfirmImport = async () => {
  296. if (importFileList.value.length === 0) {
  297. ElMessage.warning('请先选择要导入的文件');
  298. return;
  299. }
  300. const file = importFileList.value[0].raw || importFileList.value[0];
  301. if (!file) {
  302. ElMessage.warning('文件不存在');
  303. return;
  304. }
  305. // 检查文件类型
  306. const isExcel = /\.(xlsx|xls)$/.test(file.name.toLowerCase());
  307. if (!isExcel) {
  308. ElMessage.error('仅支持上传.xlsx .xls格式文件');
  309. return;
  310. }
  311. try {
  312. const formData = new FormData();
  313. formData.append('file', file);
  314. const res = await importSecurityExamineDet(formData);
  315. if (res) {
  316. handleImportSuccess({ data: res });
  317. }
  318. } catch (e: any) {
  319. console.error('导入失败:', e);
  320. ElMessage.error(e?.message || '导入失败,请重试');
  321. }
  322. };
  323. // 获取当前行的考核项目值(考虑临时输入值)
  324. const getCurrentEvaluationItem = (rowIndex: number): string => {
  325. // 如果正在输入(有临时值),使用临时值;否则使用实际值
  326. if (tempEvaluationItems.value[rowIndex] !== undefined) {
  327. return tempEvaluationItems.value[rowIndex];
  328. }
  329. return evaluationItems.value[rowIndex]?.evaluationItem || '';
  330. };
  331. // 处理单元格合并(合并考核项目列)
  332. const handleSpanMethod = ({ row, column, rowIndex, columnIndex }: any) => {
  333. // 只合并考核项目列(columnIndex === 1,因为编号列是 0)
  334. if (columnIndex === 1) {
  335. const currentItem = row.evaluationItem;
  336. if (!currentItem) {
  337. // 如果当前行的考核项目为空,不合并
  338. return {
  339. rowspan: 1,
  340. colspan: 1,
  341. };
  342. }
  343. // 查找相同考核项目的连续行
  344. let rowspan = 1;
  345. for (let i = rowIndex + 1; i < evaluationItems.value.length; i++) {
  346. if (evaluationItems.value[i].evaluationItem === currentItem) {
  347. rowspan++;
  348. } else {
  349. break;
  350. }
  351. }
  352. // 检查是否是同一组的第一行
  353. if (rowIndex > 0 && evaluationItems.value[rowIndex - 1].evaluationItem === currentItem) {
  354. // 不是第一行,不显示(被合并)
  355. return {
  356. rowspan: 0,
  357. colspan: 0,
  358. };
  359. }
  360. return {
  361. rowspan,
  362. colspan: 1,
  363. };
  364. }
  365. // 其他列不合并
  366. return {
  367. rowspan: 1,
  368. colspan: 1,
  369. };
  370. };
  371. // 检查是否是相邻行的相同内容(用于合并)
  372. const isAdjacentSameValue = (index: number, value: string): boolean => {
  373. if (!value) return false;
  374. // 检查上一行或下一行是否有相同的值
  375. const prevRow = evaluationItems.value[index - 1];
  376. const nextRow = evaluationItems.value[index + 1];
  377. return (
  378. (prevRow && prevRow.evaluationItem === value) ||
  379. (nextRow && nextRow.evaluationItem === value)
  380. );
  381. };
  382. // 检查是否有跨行的相同考核项目(非相邻的)
  383. const checkNonConsecutiveDuplicate = (index: number, value: string): boolean => {
  384. if (!value) return false;
  385. // 检查是否有非相邻行的相同值
  386. for (let i = 0; i < evaluationItems.value.length; i++) {
  387. if (i === index) continue; // 跳过当前行
  388. if (evaluationItems.value[i].evaluationItem === value) {
  389. // 检查是否是相邻行
  390. const isAdjacent = i === index - 1 || i === index + 1;
  391. if (!isAdjacent) {
  392. return true; // 发现跨行的重复
  393. }
  394. }
  395. }
  396. return false;
  397. };
  398. // 考核项目输入时的处理(只更新临时值,不触发合并)
  399. const handleEvaluationItemInput = (index: number, value: string) => {
  400. // 只更新临时值,不更新实际数据,避免实时合并
  401. tempEvaluationItems.value[index] = value;
  402. };
  403. // 考核项目失去焦点时的处理
  404. const handleEvaluationItemBlur = async (index: number) => {
  405. // 获取临时输入值
  406. const tempValue = tempEvaluationItems.value[index];
  407. const finalValue = tempValue !== undefined ? tempValue : evaluationItems.value[index].evaluationItem;
  408. // 清除临时值
  409. delete tempEvaluationItems.value[index];
  410. // 如果值为空,直接更新并返回
  411. if (!finalValue || !finalValue.trim()) {
  412. evaluationItems.value[index].evaluationItem = '';
  413. await nextTick();
  414. return;
  415. }
  416. // 先检查是否是相邻行的相同内容(允许合并)
  417. const isAdjacent = isAdjacentSameValue(index, finalValue);
  418. if (isAdjacent) {
  419. // 相邻行相同,允许合并,直接更新数据
  420. evaluationItems.value[index].evaluationItem = finalValue;
  421. await nextTick();
  422. return;
  423. }
  424. // 检查是否有跨行的相同考核项目(非相邻的)
  425. if (checkNonConsecutiveDuplicate(index, finalValue)) {
  426. ElMessage.warning('考核项目一致,请使用连续的相同考核项目');
  427. evaluationItems.value[index].evaluationItem = '';
  428. await nextTick();
  429. return;
  430. }
  431. // 没有冲突,更新实际数据
  432. evaluationItems.value[index].evaluationItem = finalValue;
  433. // 等待 DOM 更新后,强制表格重新计算合并
  434. await nextTick();
  435. };
  436. // 新增:在当前行下面插入一条新数据,如果当前行有考核项目值,新行也使用相同的值
  437. const handleAddItem = (index: number) => {
  438. const currentItem = evaluationItems.value[index];
  439. evaluationItems.value.splice(index + 1, 0, {
  440. id: 0,
  441. psemId: 0,
  442. evaluationItem: currentItem.evaluationItem || '', // 合并考核项目字段
  443. evaluationContent: '',
  444. scoringMethod: '',
  445. isAdd: currentItem.isAdd !== undefined ? currentItem.isAdd : 1, // 是否加分项,继承当前行的值
  446. reviewUserId: null, // 复评人ID
  447. reviewUserName: '', // 复评人姓名
  448. });
  449. };
  450. // 删除当前行
  451. const handleDeleteItem = (index: number) => {
  452. if (evaluationItems.value.length <= 1) {
  453. ElMessage.warning('至少需要保留一条数据');
  454. return;
  455. }
  456. evaluationItems.value.splice(index, 1);
  457. };
  458. // 查找当前行所在合并组的起始位置
  459. const findGroupStartIndex = (index: number): number => {
  460. const currentItem = evaluationItems.value[index].evaluationItem;
  461. if (!currentItem) {
  462. return index; // 如果当前行考核项目为空,直接返回当前索引
  463. }
  464. // 向上查找,找到相同考核项目的第一行
  465. let startIndex = index;
  466. for (let i = index - 1; i >= 0; i--) {
  467. if (evaluationItems.value[i].evaluationItem === currentItem) {
  468. startIndex = i;
  469. } else {
  470. break;
  471. }
  472. }
  473. return startIndex;
  474. };
  475. // 查找当前行所在合并组的结束位置
  476. const findGroupEndIndex = (index: number): number => {
  477. const currentItem = evaluationItems.value[index].evaluationItem;
  478. if (!currentItem) {
  479. return index; // 如果当前行考核项目为空,直接返回当前索引
  480. }
  481. // 向下查找,找到相同考核项目的最后一行
  482. let endIndex = index;
  483. for (let i = index + 1; i < evaluationItems.value.length; i++) {
  484. if (evaluationItems.value[i].evaluationItem === currentItem) {
  485. endIndex = i;
  486. } else {
  487. break;
  488. }
  489. }
  490. return endIndex;
  491. };
  492. // 向上插入分类:在当前行所在合并组的第一行之前插入一条空白数据
  493. const handleMoveUp = (index: number) => {
  494. const groupStartIndex = findGroupStartIndex(index);
  495. evaluationItems.value.splice(groupStartIndex, 0, {
  496. id: 0,
  497. psemId: 0,
  498. evaluationItem: '',
  499. evaluationContent: '',
  500. scoringMethod: '',
  501. isAdd: 1, // 是否加分项(0-否,1-是),默认为1
  502. reviewUserId: null, // 复评人ID
  503. reviewUserName: '', // 复评人姓名
  504. });
  505. };
  506. // 向下插入分类:在当前行所在合并组的最后一行之后插入一条空白数据
  507. const handleMoveDown = (index: number) => {
  508. const groupEndIndex = findGroupEndIndex(index);
  509. evaluationItems.value.splice(groupEndIndex + 1, 0, {
  510. id: 0,
  511. psemId: 0,
  512. evaluationItem: '',
  513. evaluationContent: '',
  514. scoringMethod: '',
  515. isAdd: 1, // 是否加分项(0-否,1-是),默认为1
  516. reviewUserId: null, // 复评人ID
  517. reviewUserName: '', // 复评人姓名
  518. });
  519. };
  520. // 将逗号分隔的URL字符串转换为FileItem数组
  521. const convertAttachmentsToFileItems = (attachmentsStr: string): FileItem[] => {
  522. if (!attachmentsStr || !attachmentsStr.trim()) {
  523. return [];
  524. }
  525. // 按逗号分割URL
  526. const urls = attachmentsStr.split(',').map(url => url.trim()).filter(url => url);
  527. return urls.map((url, index) => {
  528. // 从URL中提取文件名
  529. const urlParts = url.split('/');
  530. const fileName = urlParts[urlParts.length - 1] || `附件${index + 1}`;
  531. // 根据文件扩展名判断文件类型
  532. const extension = fileName.split('.').pop()?.toLowerCase() || '';
  533. let fileType = 'pdf';
  534. if (extension === 'doc' || extension === 'docx') {
  535. fileType = 'word';
  536. } else if (extension === 'xls' || extension === 'xlsx') {
  537. fileType = 'excel';
  538. } else if (extension === 'ppt' || extension === 'pptx') {
  539. fileType = 'ppt';
  540. }
  541. return {
  542. fileId: 0,
  543. fileName,
  544. fileType,
  545. fileSize: '0',
  546. fileUrl: url,
  547. };
  548. });
  549. };
  550. const getDetail = async () => {
  551. if (!props.id) return;
  552. try {
  553. const detail = await querySecurityExamineDetail(props.id);
  554. if (!detail) return;
  555. // 填充表单数据
  556. ruleFormData.value.evaluationTableName = detail.exName || '';
  557. ruleFormData.value.scoringDescription = detail.ratingDescribe || '';
  558. // 转换附件文档:将逗号分隔的URL字符串转换为FileItem数组
  559. ruleFormData.value.attachmentDocument = convertAttachmentsToFileItems(detail.attachments || '');
  560. // 填充考核项目列表
  561. if (detail.exContents && detail.exContents.length > 0) {
  562. // 详情接口返回的数据视为已持久化的数据,保留 id / psemId
  563. evaluationItems.value = detail.exContents.map((item: any) => ({
  564. id: item.id || 0,
  565. psemId: item.psemId || 0,
  566. evaluationItem: item.exProgram || '',
  567. evaluationContent: item.exContent || '',
  568. scoringMethod: item.scoringWay || '',
  569. isAdd: item.isAdd !== undefined ? item.isAdd : 1, // 是否加分项(0-否,1-是),默认为1
  570. reviewUserId: item.reviewUserId || null, // 复评人ID
  571. reviewUserName: item.reviewUserName || '', // 复评人姓名
  572. }));
  573. } else {
  574. // 如果没有数据,至少保留一条空白数据
  575. evaluationItems.value = [
  576. {
  577. id: 0,
  578. psemId: 0,
  579. evaluationItem: '',
  580. evaluationContent: '',
  581. scoringMethod: '',
  582. isAdd: 1, // 是否加分项(0-否,1-是),默认为1
  583. reviewUserId: null, // 复评人ID
  584. reviewUserName: '', // 复评人姓名
  585. },
  586. ];
  587. }
  588. } catch (e: any) {
  589. console.error('获取详情失败:', e);
  590. ElMessage.error(e?.message || '获取详情失败,请重试');
  591. }
  592. };
  593. // 验证考核项目列表
  594. const validateEvaluationItems = (): boolean => {
  595. for (let i = 0; i < evaluationItems.value.length; i++) {
  596. const item = evaluationItems.value[i];
  597. if (!item.evaluationItem || !item.evaluationItem.trim()) {
  598. ElMessage.warning(`第 ${i + 1} 行的考核项目不能为空`);
  599. return false;
  600. }
  601. if (!item.evaluationContent || !item.evaluationContent.trim()) {
  602. ElMessage.warning(`第 ${i + 1} 行的考核内容不能为空`);
  603. return false;
  604. }
  605. }
  606. return true;
  607. };
  608. const handleSubmit = async () => {
  609. const res = await handleValidate();
  610. if (!res) return;
  611. // 验证考核项目列表
  612. if (!validateEvaluationItems()) {
  613. return;
  614. }
  615. try {
  616. // 处理附件文档:先上传文件获取 URL,然后提取 fileUrl,多个用逗号分隔
  617. let attachments = '';
  618. if (ruleFormData.value.attachmentDocument && ruleFormData.value.attachmentDocument.length > 0) {
  619. // 分离已有URL的文件和新上传的文件
  620. const existingFiles: string[] = [];
  621. const newFiles: any[] = [];
  622. ruleFormData.value.attachmentDocument.forEach((file: any) => {
  623. // 如果文件已经有 fileUrl 且没有 file 对象,说明是已有文件
  624. if (file.fileUrl && !file.file) {
  625. existingFiles.push(file.fileUrl);
  626. } else {
  627. // 否则是需要上传的新文件
  628. newFiles.push(file);
  629. }
  630. });
  631. // 上传新文件
  632. let uploadedUrls: string[] = [];
  633. if (newFiles.length > 0) {
  634. const uploadedFiles = await formatAttachmentList(newFiles);
  635. uploadedUrls = uploadedFiles
  636. .map((file: any) => file.fileUrl || file.url || '')
  637. .filter((url: string) => url);
  638. }
  639. // 合并已有URL和新上传的URL
  640. attachments = [...existingFiles, ...uploadedUrls].filter((url: string) => url).join(',');
  641. }
  642. // 映射考核项目列表,添加序号
  643. const exContents: EvaluationContent[] = evaluationItems.value.map((item, index) => {
  644. const base: any = {
  645. serialNum: index + 1, // 序号从1开始
  646. exProgram: item.evaluationItem || '', // 考核项目
  647. exContent: item.evaluationContent || '', // 考核内容
  648. scoringWay: item.scoringMethod || '', // 评分方式
  649. isAdd: item.isAdd !== undefined ? item.isAdd : 1, // 是否加分项(0-否,1-是)
  650. reviewUserId: item.reviewUserId || null, // 复评人ID
  651. reviewUserName: item.reviewUserName || '', // 复评人姓名
  652. };
  653. // 只有详情接口返回的已存在数据才携带 id / psemId
  654. if (item.id && item.psemId) {
  655. base.id = item.id;
  656. base.psemId = item.psemId;
  657. }
  658. return base as EvaluationContent;
  659. });
  660. if (isEditMode.value && props.id) {
  661. // 编辑模式:调用更新接口
  662. const payload = {
  663. id: props.id, // 编辑时必须传ID
  664. exName: ruleFormData.value.evaluationTableName || '', // 考核表名称
  665. attachments, // 考核文档
  666. ratingDescribe: ruleFormData.value.scoringDescription || '', // 评分说明
  667. deptNames: '', // 下发部门(编辑时为空)
  668. deptIds: [], // 下发部门ID数组(编辑时为空)
  669. getUserGroupId: 0,
  670. planStartTime: '', // 计划开始时间(编辑时为空)
  671. planEndTime: '', // 计划结束时间(编辑时为空)
  672. status: 0, // 状态:0-未下发
  673. createdUserId: userId || 0, // 创建人ID
  674. createdUserName: userName || '', // 创建人名称
  675. exContents, // 考核项目列表
  676. };
  677. await updateSecurityExamine(payload);
  678. ElMessage.success('保存成功');
  679. } else {
  680. // 创建模式:调用保存接口
  681. const payload = {
  682. id: 0, // 新增时为0
  683. exName: ruleFormData.value.evaluationTableName || '', // 考核表名称
  684. attachments, // 考核文档
  685. ratingDescribe: ruleFormData.value.scoringDescription || '', // 评分说明
  686. deptNames: '', // 下发部门(创建时为空)
  687. deptIds: [], // 下发部门ID数组(创建时为空)
  688. getUserGroupId: 0,
  689. planStartTime: '', // 计划开始时间(创建时为空)
  690. planEndTime: '', // 计划结束时间(创建时为空)
  691. status: 0, // 状态:0-未下发
  692. createdUserId: userId || 0, // 创建人ID
  693. createdUserName: userName || '', // 创建人名称
  694. exContents, // 考核项目列表
  695. };
  696. await saveSecurityExamine(payload);
  697. ElMessage.success('创建成功');
  698. }
  699. router.back();
  700. } catch (e: any) {
  701. console.error('保存失败:', e);
  702. ElMessage.error(e?.message || '保存失败,请重试');
  703. }
  704. };
  705. onMounted(() => {
  706. // 获取复评人列表
  707. getReviewUserList();
  708. if (isEditMode.value || isViewMode.value) {
  709. getDetail();
  710. } else {
  711. // 创建模式下,确保附件文档字段为空
  712. ruleFormData.value.attachmentDocument = [];
  713. // 创建模式下,确保至少有一条空白数据
  714. if (evaluationItems.value.length === 0) {
  715. evaluationItems.value = [
  716. {
  717. id: 0,
  718. psemId: 0,
  719. evaluationItem: '',
  720. evaluationContent: '',
  721. scoringMethod: '',
  722. isAdd: 1, // 是否加分项(0-否,1-是),默认为1
  723. reviewUserId: null, // 复评人ID
  724. reviewUserName: '', // 复评人姓名
  725. },
  726. ];
  727. }
  728. }
  729. });
  730. </script>
  731. <style scoped lang="scss">
  732. @use '@/styles/page-details-layout.scss' as *;
  733. .evaluation-form {
  734. display: flex;
  735. flex-direction: column;
  736. width: 600px;
  737. gap: 32px;
  738. :deep(.el-form-item) {
  739. margin-bottom: 0;
  740. }
  741. :deep(.el-form-item__label) {
  742. padding: 0;
  743. }
  744. }
  745. .evaluation-items-section {
  746. margin-top: 32px;
  747. }
  748. .section-header {
  749. display: flex;
  750. gap: 10px;
  751. margin-bottom: 20px;
  752. }
  753. .evaluation-items-table {
  754. width: 100%;
  755. }
  756. </style>