EvaluationSystemDetail.vue 29 KB

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