华西海圻ELN前端工程
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1363 lines
50 KiB

  1. <template>
  2. <div class="flex flex1">
  3. <div class="flex1 flex">
  4. <el-input v-if="type === 'input'" :maxlength="item.maxlength || 50" :disabled="getDisabled()"
  5. :class="getFillTypeStyle() + (orangeBg ? ' orange-bg' : '')" @blur="onBlur"
  6. :placeholder="getPlaceholder()" v-model="inputValue" @input="onInputChange" @change="onInputChange" />
  7. <el-input v-else-if="type === 'textarea'" :maxlength="item.maxlength || 50" :disabled="getDisabled()"
  8. :class="getFillTypeStyle() + (orangeBg ? ' orange-bg' : '')" type="textarea" show-word-limit
  9. resize="none" @blur="onBlur" :rows="item.rows || 3" :placeholder="getPlaceholder()" v-model="inputValue"
  10. @input="onInputChange" @change="onInputChange" />
  11. <DecimalInput v-else-if="type === 'inputNumber'" @blur="onCommonHandleSaveRecord"
  12. :maxlength="item.maxlength || 10" class="flex1" :disabled="getDisabled()"
  13. :controls="item.controls || false" :min="item.min || 0" :prepend="item.prepend"
  14. :decimalDigits="item.precision || 6" :class="getFillTypeStyle() + (orangeBg ? ' orange-bg' : '')"
  15. :placeholder="getPlaceholder()" v-model="inputValue" @input="onInputChange" @change="onInputChange" />
  16. <el-select v-else-if="type === 'select'" class="flex1" :multiple="item.multiple"
  17. :class="getFillTypeStyle() + (orangeBg ? ' orange-bg' : '')" v-model="inputValue"
  18. :disabled="getDisabled()" :placeholder="getPlaceholder()" @remove-tag="onRemoveTag"
  19. @visible-change="onSelectBlur" @change="onInputChange">
  20. <el-option v-for="op in item.options" :key="op.value" :label="op.label" :value="op.value">
  21. </el-option>
  22. </el-select>
  23. <el-checkbox v-else-if="type === 'checkbox'" class="flex1" :multiple="item.multiple"
  24. :class="getFillTypeStyle() + (orangeBg ? ' orange-bg' : '')" v-model="inputValue"
  25. :disabled="getDisabled()" :placeholder="getPlaceholder()" @change="onItemCheckboxChange">
  26. </el-checkbox>
  27. <el-date-picker v-else-if="type === 'dateTime'" type="datetime" class="flex1"
  28. :class="getFillTypeStyle() + (orangeBg ? ' orange-bg' : '')" v-model="inputValue"
  29. :disabled="getDisabled()" format="yyyy/MM/dd HH:mm:ss" :placeholder="getPlaceholder()"
  30. @change="(val) => onDateChange(val, 'yyyy/MM/DD HH:mm:ss')">
  31. </el-date-picker>
  32. <el-date-picker v-else-if="type === 'datePicker'" class="flex1"
  33. :class="getFillTypeStyle() + (orangeBg ? ' orange-bg' : '')" v-model="inputValue"
  34. :disabled="getDisabled()" format="yyyy/MM/dd" :placeholder="getPlaceholder()"
  35. @change="(val) => onDateChange(val, 'yyyy/MM/DD')">
  36. </el-date-picker>
  37. <el-button v-else-if="type === 'button'" :class="getFillTypeStyle() + (orangeBg ? ' orange-bg' : '')"
  38. :disabled="getDisabled()" type="primary" @click="handleClickButton(item)">
  39. {{ item.buttonName }}
  40. </el-button>
  41. <div class="clickable"
  42. :class="getFillTypeStyle() + (getDisabled() ? ' disabled' : '') + (orangeBg ? ' orange-bg' : '')"
  43. v-else-if="item.type === 'clickable'" @click="handleClickable(item, $event)">
  44. <span v-if="inputValue">{{ inputValue }}</span>
  45. <span v-else class="default-placeholder-text">{{ getPlaceholder() }}</span>
  46. </div>
  47. <div class="clickable"
  48. :class="getFillTypeStyle() + (getDisabled() ? ' disabled' : '') + (orangeBg ? ' orange-bg' : '')"
  49. v-else-if="regentType.includes(item.type)" @click="onCommonHandleRegent(item, item.type)">
  50. <span v-if="inputValue">{{ inputValue }}</span>
  51. <span v-else class="default-placeholder-text">{{ getPlaceholder() }}</span>
  52. </div>
  53. <template v-else-if="type === 'attachment'">
  54. <el-upload ref="uploadRef" class="upload-demo" :action="uploadFileUrl" :on-preview="handlePreview"
  55. :headers="headers"
  56. :before-remove="beforeRemove"
  57. :on-remove="handleRemove" multiple :limit="10"
  58. :on-success="handleSuccess"
  59. :on-change="handleChange"
  60. :on-exceed="handleExceed" :file-list="fileList"
  61. :auto-upload="false">
  62. <el-button :class="getFillTypeStyle() + (orangeBg ? ' orange-bg' : '')" size="small" type="primary">点击上传</el-button>
  63. <span v-if="error" class = "atta-tips">请上传附件</span>
  64. <div slot="tip" class="el-upload__tip">支持扩展名.rar .zip .doc .docx .pdf .jpg文件大小不超过2MB</div>
  65. </el-upload>
  66. </template>
  67. </div>
  68. <div class="handle-row" v-if="isShowHandle()">
  69. <el-checkbox v-model="checkboxValue" v-if="getIsShowCheckboxIcon()" :disabled="getCheckboxDisabled()"
  70. class="mr-5" @change="onCheckboxChange"></el-checkbox>
  71. <div class="handle-icon" v-if="getIsShowQuestionIcon()" @click="onClickQuestion"
  72. @mouseenter="(e) => onMouseEnter('replyRecord', e)" @mouseleave="onMouseLeave">
  73. <Question :class="getQuestionColor()" />
  74. </div>
  75. <img v-if="getIsShowCopyIcon()" @click="onCopy" src="@/assets/images/copy-icon.svg" class="handle-icon"
  76. alt="" />
  77. <img v-if="getIsShowRecordIcon()" @mouseenter="(e) => onMouseEnter('fieldChanged', e)"
  78. @mouseleave="onMouseLeave" src="@/assets/images/record-icon.svg" class="handle-icon" alt="" />
  79. </div>
  80. <!-- 修改记录模态框 -->
  81. <div v-if="showModal && modificationRecords.length > 0" ref="modalRef"
  82. :class="['modification-modal', { 'show': showModal }]" @mouseenter="onModalEnter"
  83. @mouseleave="onModalLeave">
  84. <div class="modal-content">
  85. <div class="records-list">
  86. <div v-for="(record, index) in modificationRecords" :key="index" class="record-item">
  87. <!-- 字段修改记录 -->
  88. <div class="record-row" v-if="currentRecordType === 'fieldChanged'">
  89. <div>
  90. <span>{{ index + 1 }}.</span>
  91. <span> {{ getUserName(record) }} </span>
  92. <span>{{ record.time }} </span>
  93. <span>{{ modificationRecords.length - 1 == index ? "提交" : "修改" }}</span>
  94. </div>
  95. <div v-if="modificationRecords.length - 1 !== index">
  96. <div>原值{{ record.oldValue }}</div>
  97. <div>修改值{{ record.value }}</div>
  98. <div v-if="record.reason">备注{{ record.reason }}</div>
  99. </div>
  100. </div>
  101. <!-- 回复记录 -->
  102. <div class="record-row" v-if="currentRecordType === 'replyRecord'">
  103. <div>
  104. <span> {{ getUserName(record) }} </span>
  105. <span>{{ record.time }} </span>
  106. </div>
  107. <div>
  108. <div v-if="record.content">复核意见{{ record.content }}</div>
  109. <div v-if="record.reply">回复意见{{ record.reply }}</div>
  110. </div>
  111. </div>
  112. <hr v-if="index < modificationRecords.length - 1">
  113. </div>
  114. </div>
  115. </div>
  116. </div>
  117. <el-dialog :close-on-click-modal="false" append-to-body :title="templateFillType == 'actFill' ? '回复意见' : '复核意见'"
  118. :visible.sync="visible" width="30%">
  119. <el-input v-model="replyContent" type="textarea" show-word-limit resize="none" rows="8" placeholder="输入内容"
  120. maxlength="500" />
  121. <span slot="footer" class="dialog-footer">
  122. <el-button @click="visible = false"> </el-button>
  123. <el-button type="primary" @click="onReplyConfirm"> </el-button>
  124. </span>
  125. </el-dialog>
  126. </div>
  127. </template>
  128. <script>
  129. import Question from "./icons/Question.vue";
  130. import DecimalInput from "./DecimalInput.vue";
  131. import { EventBus } from "@/utils/eventBus";
  132. import moment from "moment";
  133. import { deepClone } from "@/utils/index";
  134. import { getuuid } from "@/utils/index.js";
  135. import { getToken } from "@/utils/auth"
  136. export default {
  137. inject: ['templateData', 'templateFillType', "getZdxgjl", "getFhyjjl", "updateZdxgjl", "replaceFhyjjl", "updateFhyjjl", "getFieldCheckObj", "updateFieldCheckObj"],
  138. components: {
  139. Question,
  140. DecimalInput,
  141. },
  142. props: {
  143. type: {//form类型 input/select等
  144. type: String,
  145. default: "input"
  146. },
  147. item: {
  148. type: Object,
  149. default: () => {
  150. return {
  151. placeholder: "",
  152. maxlength: 30,
  153. label: "",
  154. disabled: false,
  155. }
  156. }
  157. },
  158. // v-model 值
  159. value: {
  160. type: [String, Number, Array, Boolean],
  161. default: ''
  162. },
  163. // 错误状态
  164. error: {
  165. type: Boolean,
  166. default: false
  167. },
  168. // 橙色背景状态
  169. orangeBg: {
  170. type: Boolean,
  171. default: false
  172. },
  173. fieldKey: {
  174. type: String,
  175. default: ""
  176. },
  177. fieldItemLabel: {
  178. type: String,
  179. default: "",
  180. },
  181. //是否记录修改
  182. isFieldsRecord: {
  183. type: Boolean,
  184. default: true,
  185. },
  186. sourceFrom: {
  187. type: String,
  188. default: ""
  189. },
  190. },
  191. data() {
  192. return {
  193. inputValue: this.value,
  194. oldValue: this.value, // 记录上一次的值
  195. showModal: false, // 控制模态框显示
  196. modificationRecords: [], // 存储修改记录
  197. modalTimer: null, // 用于延迟隐藏模态框
  198. isHoveringModal: false, // 是否悬停在模态框上
  199. isHoveringMain: false, // 是否悬停在主元素上(这个实际上不需要,因为我们有事件处理)
  200. currentRecordType: '', // 当前悬停的记录类型(replyRecord 或 modifyRecord)
  201. replyContent: '', // 回复内容
  202. visible: false,//是否显示弹窗
  203. checkboxValue: this.getChecked(),//是否选中
  204. uuid: getuuid(), // 唯一标识符,用于EventBus事件匹配
  205. regentType: ['sj', 'gsp', 'mix', 'xj', 'xb', 'gyzj', 'mjy', 'yq','jcb'], //试剂/仪器/供试品等类型
  206. selectRegentInfo: {},//选择的试剂/仪器/供试品等信息
  207. fileList: [],//上传的文件列表
  208. uploadFileUrl: process.env.VUE_APP_BASE_API + "/file/upload",
  209. headers: {
  210. Authorization: "Bearer " + getToken(),
  211. },
  212. pendingUploadFile: null, // 用于存储待上传的文件
  213. pendingRemoveFile: null, // 用于存储待删除的文件
  214. }
  215. },
  216. watch: {
  217. value(newVal) {
  218. this.inputValue = newVal;
  219. }
  220. },
  221. filters: {
  222. },
  223. mounted() {
  224. if(this.item.type === 'attachment'){
  225. try{
  226. this.fileList = JSON.parse(this.value);
  227. }catch(e){
  228. this.fileList = [];
  229. }
  230. }
  231. EventBus.$on('onExternalFieldUpdate', this.handleExternalFieldUpdate);
  232. EventBus.$on('onEditSignCancel', this.handleEditSignCancel);
  233. EventBus.$on('onEditSignCallback', this.handleEditSignCallback);
  234. //试剂
  235. EventBus.$on("onReagentSubmit", this.onReagentSubmit)
  236. //仪器
  237. EventBus.$on("onInstrumentSubmit", this.onMixReagentSubmit)
  238. //供试品/试剂/给药制剂等
  239. EventBus.$on("onMixReagentSubmit", this.onMixReagentSubmit)
  240. },
  241. unmounted() {
  242. EventBus.$off('onExternalFieldUpdate', this.handleExternalFieldUpdate);
  243. EventBus.$off('onEditSignCancel', this.handleEditSignCancel);
  244. EventBus.$off('onEditSignCallback', this.handleEditSignCallback);
  245. EventBus.$off("onReagentSubmit", this.onReagentSubmit)
  246. EventBus.$off("onInstrumentSubmit", this.onMixReagentSubmit)
  247. EventBus.$off("onMixReagentSubmit", this.onMixReagentSubmit)
  248. },
  249. methods: {
  250. onInstrumentSubmit(data){
  251. if(data.uuid !== this.uuid) return;
  252. this.selectRegentInfo = data;
  253. console.log(data,"yq")
  254. },
  255. // 文件状态改变时的钩子,添加文件、上传成功、上传失败时都会被调用
  256. handleChange(file, fileList) {
  257. // 如果是新添加的文件(status为ready),进行验证
  258. if (file.status === 'ready') {
  259. const isAllowedType = ['image/jpeg', 'image/png', 'image/gif', 'application/pdf',
  260. 'application/x-rar-compressed', 'application/zip',
  261. 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'].includes(file.raw.type)
  262. const isLt2M = file.size / 1024 / 1024 < 2
  263. if (!isAllowedType) {
  264. this.$message.error(`文件 ${file.name} 格式不支持!只能上传 JPG/PNG/GIF/PDF/RAR/ZIP/DOC/DOCX 格式的文件`)
  265. // 从fileList中移除该文件
  266. const index = fileList.indexOf(file);
  267. if (index > -1) {
  268. fileList.splice(index, 1);
  269. }
  270. this.fileList = [...fileList];
  271. return
  272. }
  273. if (!isLt2M) {
  274. this.$message.error(`文件 ${file.name} 大小超过2MB!`)
  275. // 从fileList中移除该文件
  276. const index = fileList.indexOf(file);
  277. if (index > -1) {
  278. fileList.splice(index, 1);
  279. }
  280. this.fileList = [...fileList];
  281. return
  282. }
  283. // 如果已经有文件在列表中(不包括当前新添加的),需要验证电子签名
  284. const existingFiles = fileList.filter(f => f !== file && f.status === 'success');
  285. if (existingFiles.length > 0) {
  286. // 保存待上传的文件信息
  287. this.pendingUploadFile = file;
  288. // 从fileList中暂时移除新文件,等待签名验证
  289. const index = fileList.indexOf(file);
  290. if (index > -1) {
  291. fileList.splice(index, 1);
  292. }
  293. this.fileList = [...fileList];
  294. // 触发电子签名弹窗
  295. EventBus.$emit('showEditSignDialog', { uuid: this.uuid });
  296. return;
  297. }
  298. // 没有现有文件或验证通过,手动提交上传
  299. this.$nextTick(() => {
  300. // 找到对应的upload组件并提交
  301. const uploadComponent = this.$refs.uploadRef;
  302. if (uploadComponent) {
  303. uploadComponent.submit();
  304. }
  305. });
  306. }
  307. // 更新fileList
  308. this.fileList = fileList;
  309. },
  310. handleSuccess(res, file, fileList) {
  311. if(res.code == 200){
  312. this.fileList = fileList;
  313. // 更新inputValue为文件路径列表,方便后续保存
  314. this.inputValue = JSON.stringify(this.getFileList(fileList));
  315. this.$emit("change",this.inputValue)
  316. this.onCommonHandleSaveRecord();
  317. this.$message.success('文件上传成功');
  318. } else {
  319. this.$message.error(res.message || '文件上传失败');
  320. // 上传失败,从列表中移除
  321. const index = fileList.indexOf(file);
  322. if (index > -1) {
  323. fileList.splice(index, 1);
  324. this.fileList = [...fileList];
  325. }
  326. }
  327. },
  328. getFileList(fileList) {
  329. const list = [];
  330. fileList.forEach(item => {
  331. const o = {name:item.name};
  332. if(item.url){//回填的数据
  333. o.url = item.url
  334. }else{//新上传的
  335. o.url = item.response.data.url
  336. }
  337. list.push(o)
  338. })
  339. return list;
  340. },
  341. // 删除前验证电子签名
  342. beforeRemove(file) {
  343. // 所有删除操作都需要验证电子签名
  344. // 保存待删除的文件信息
  345. this.pendingRemoveFile = { file, fileList: this.fileList };
  346. // 触发电子签名弹窗
  347. EventBus.$emit('showEditSignDialog', { uuid: this.uuid });
  348. // 返回false阻止默认删除行为,等待签名验证通过后再删除
  349. return false;
  350. },
  351. handleRemove(file, fileList) {
  352. // on-remove事件在before-remove返回false时不会触发
  353. // 这个方法在签名验证通过后通过executeRemove调用
  354. // 这里不需要额外处理,因为executeRemove已经处理了删除逻辑
  355. },
  356. // 执行实际的文件删除操作
  357. executeRemove(file, fileList) {
  358. // 从el-upload的内部uploadFiles中移除文件
  359. const uploadComponent = this.$refs.uploadRef;
  360. if (uploadComponent && uploadComponent.uploadFiles) {
  361. const uploadFileIndex = uploadComponent.uploadFiles.indexOf(file);
  362. if (uploadFileIndex > -1) {
  363. uploadComponent.uploadFiles.splice(uploadFileIndex, 1);
  364. }
  365. }
  366. // 从fileList中移除文件
  367. const index = fileList.indexOf(file);
  368. if (index > -1) {
  369. fileList.splice(index, 1);
  370. }
  371. // 更新fileList
  372. this.fileList = [...fileList];
  373. // 更新inputValue为剩余文件路径列表
  374. this.inputValue = JSON.stringify(this.getFileList(fileList));
  375. this.$emit("change", this.inputValue);
  376. // 触发保存记录
  377. this.onCommonHandleSaveRecord();
  378. this.$message.success(`文件 ${file.name} 已移除`);
  379. },
  380. handlePreview(file) {
  381. console.log(file)
  382. const url = file.url || file.response?.data?.url;
  383. if (url) {
  384. window.open(process.env.VUE_APP_FILE_DOMAIN + url, '_blank');
  385. }
  386. },
  387. handleExceed(files, fileList) {
  388. this.$message.warning(`当前限制选择 10 个文件,本次选择了 ${files.length} 个文件,共选择了 ${files.length + fileList.length} 个文件`);
  389. },
  390. //试剂弹窗提交
  391. onMixReagentSubmit(data) {
  392. if (data.uuid !== this.uuid) return;
  393. // 创建一个验证控制器,用于收集各级验证结果
  394. const validationController = {
  395. isPrevented: false,
  396. errorMsg: '',
  397. prevent(msg) {
  398. this.isPrevented = true;
  399. if (msg) this.errorMsg = msg;
  400. }
  401. };
  402. // 触发自定义验证事件,让父组件判断是否满足需求
  403. // 父组件可以通过validationController.prevent(msg)来阻止提交
  404. this.$emit('beforeReagentSubmit', data, validationController);
  405. // 检查是否被阻止
  406. if (validationController.isPrevented) {
  407. // 验证不通过,显示错误信息
  408. if (validationController.errorMsg) {
  409. this.$message.error(validationController.errorMsg);
  410. }
  411. return;
  412. }
  413. // 验证通过,继续执行
  414. this.executeReagentSubmit(data);
  415. },
  416. // 执行试剂提交的共同逻辑
  417. executeReagentSubmit(data) {
  418. this.inputValue = data.selectedId;
  419. this.selectRegentInfo = data;
  420. EventBus.$emit("hideSelectMixReagentDialog");
  421. this.onCommonHandleSaveRecord(this.inputValue);
  422. },
  423. //统一处理试剂/供试品等弹窗
  424. onCommonHandleRegent(item, type) {
  425. if (this.templateFillType !== 'actFill') {
  426. return
  427. }
  428. let params = {
  429. studyFormId: this.templateData.id,
  430. uuid: this.uuid,
  431. sourceFrom: this.sourceFrom,
  432. }
  433. let eventName = "showSelectMixReagentDialog";
  434. if (type === "yq") {
  435. eventName = "showSelectInstrumentDialog";
  436. }else {
  437. const sjType = {
  438. sj: "1",//试剂
  439. gsp: "7",//供试品
  440. mix: "1",//试剂/供试品/试剂
  441. gyzj: "3",//给药制剂
  442. mjy: "5",//麻精药
  443. xj: "9",//细菌
  444. xb: "11",//细胞
  445. jcb: "13",//检测板
  446. }
  447. params = {
  448. ...params,
  449. type: sjType[type]
  450. };
  451. if(type === "mix"){
  452. params.mixType = true;
  453. }
  454. }
  455. EventBus.$emit(eventName, params)
  456. // this.$emit('regent', item,type);
  457. },
  458. // 点击按钮
  459. handleClickButton(item) {
  460. this.$emit('clickButton', item);
  461. },
  462. onDateChange(val, format) {
  463. this.inputValue = moment(val).format(format);
  464. console.log(this.inputValue, "inputValue")
  465. this.onCommonHandleSaveRecord(this.inputValue);
  466. },
  467. getUserName(record) {
  468. const locale = this.$i18n.locale;
  469. if (locale === 'zh_CN') {
  470. return record.userNameCn;
  471. }
  472. return record.userNameEn;
  473. },
  474. // 处理电子签名取消事件
  475. handleEditSignCancel(data) {
  476. if (data.uuid === this.uuid) {
  477. // 如果有待上传的文件,清空它
  478. if (this.pendingUploadFile) {
  479. this.pendingUploadFile = null;
  480. this.$message.info('已取消文件上传');
  481. } else if (this.pendingRemoveFile) {
  482. // 如果有待删除的文件,清空它
  483. this.pendingRemoveFile = null;
  484. this.$message.info('已取消文件删除');
  485. } else {
  486. this.resetRecord();
  487. }
  488. }
  489. },
  490. // 处理电子签名确认回调事件
  491. handleEditSignCallback(data) {
  492. if (data.uuid === this.uuid) {
  493. this.onEditSignSave(data.data);
  494. }
  495. },
  496. onEditSignSave(data) {
  497. // 检查是否有待上传的文件
  498. if (this.pendingUploadFile) {
  499. // 签名验证通过,将文件添加到列表并上传
  500. const file = this.pendingUploadFile;
  501. // 将文件添加到 fileList
  502. this.fileList.push(file);
  503. // 清空待上传文件标记
  504. this.pendingUploadFile = null;
  505. // 手动触发上传
  506. this.$nextTick(() => {
  507. const uploadComponent = this.$refs.uploadRef;
  508. if (uploadComponent) {
  509. uploadComponent.submit();
  510. }
  511. });
  512. } else if (this.pendingRemoveFile) {
  513. // 签名验证通过,执行文件删除
  514. const { file, fileList } = this.pendingRemoveFile;
  515. // 清空待删除文件标记
  516. this.pendingRemoveFile = null;
  517. // 执行删除操作
  518. this.executeRemove(file, fileList);
  519. } else {
  520. // 没有待上传/删除文件,执行正常的记录更新
  521. this.handleUpdateRecord(data);
  522. }
  523. },
  524. getChecked() {
  525. return !!this.getFieldCheckObj()[this.fieldKey]?.checked;
  526. },
  527. getFillTypeStyle(type) {
  528. const { fillType } = this.item;
  529. const typeObj = {
  530. actFill: "orange-border",//实际填写的边框颜色
  531. green: "green-border",
  532. preFill: "blue-border",//预填写的边框颜色
  533. }
  534. // 如果有错误状态,返回红色边框样式,覆盖原有的边框颜色
  535. if (this.error && this.type !== "attachment") {
  536. return "error-border";
  537. }
  538. return typeObj[fillType] || ""
  539. },
  540. //确认回复
  541. onReplyConfirm() {
  542. if (!this.replyContent) {
  543. this.$message({
  544. message: '请输入内容',
  545. type: 'error'
  546. });
  547. return;
  548. }
  549. const baseInfo = this.getCommonRecordInfo();
  550. const record = {
  551. ...baseInfo,
  552. title: this.templateFillType == 'actFill' ? "回复意见" : "复核意见",
  553. time: moment().format("YYYY-MM-DD HH:mm:ss"),
  554. }
  555. if (this.templateFillType == 'actFill') {
  556. record.reply = this.replyContent;
  557. const deepList = deepClone(this.getFhyjjl());//实际填报应该是修改指定的字段
  558. const item = deepList.find(o => o.key == record.key);
  559. if (item) {
  560. item.reply = this.replyContent;
  561. }
  562. this.replaceFhyjjl(deepList);//实际填报应该是修改指定的字段
  563. } else {
  564. const records = this.getReplyRecords();
  565. record.content = this.replyContent;
  566. if (records.length > 0) {
  567. const o = records[0];
  568. if (o.reply && o.content) {//如果填报人员已回复,那么就产生一条新的记录。
  569. this.updateFhyjjl(record);//qc直接插入数据源
  570. } else {//如果填报人员未填报,只更新当条记录的复核内容
  571. const deepList = deepClone(this.getFhyjjl());
  572. const item = deepList.find(it => it.key == record.key);
  573. if (item) {
  574. item.content = this.replyContent;
  575. }
  576. this.replaceFhyjjl(deepList);
  577. }
  578. } else {
  579. this.updateFhyjjl(record);//qc直接插入数据源
  580. }
  581. }
  582. const params = {
  583. //reply:回复,content:复核
  584. type: this.templateFillType == 'actFill' ? "reply" : "content",
  585. newRecord: [record],
  586. resourceList: this.getFhyjjl(),
  587. }
  588. // 触发回复记录事件
  589. EventBus.$emit('onModifyRecord', params);
  590. // 清空回复内容
  591. this.replyContent = '';
  592. // 隐藏弹窗
  593. this.visible = false;
  594. },
  595. //获取question图标颜色
  596. getQuestionColor() {
  597. const records = this.getReplyRecords();
  598. if (records.length > 0) {
  599. const o = records[0];
  600. if (o.reply && o.content) {//有回复意见和复核意见
  601. return "green"
  602. } else if (o.content && !o.reply) {//只有复核意见
  603. return "orange"
  604. } else {
  605. return "gray"
  606. }
  607. } else {//没有回复记录
  608. return "gray"
  609. }
  610. },
  611. // 复选框变化处理
  612. onCheckboxChange(val) {
  613. //有提出意见就不能勾选
  614. if (this.templateFillType == 'qc' && this.getQuestionColor() === "orange") {
  615. this.checkboxValue = false;
  616. this.$message({
  617. message: '该表单还有质疑项未处理,无法勾选',
  618. type: 'error'
  619. });
  620. return;
  621. }
  622. this.checkboxValue = val;
  623. // 触发修改记录事件
  624. EventBus.$emit('onModifyRecord', {
  625. type: "checkbox",
  626. fieldCheckObj: JSON.stringify({ ...this.getFieldCheckObj(), [this.fieldKey]: { checked: val } }),//复选框状态对象
  627. });
  628. this.updateFieldCheckObj({ [this.fieldKey]: { checked: val } });
  629. // this.$emit('input', val);
  630. // this.$emit('change', val);
  631. },
  632. onRemoveTag(e) {
  633. this.onCommonHandleSaveRecord(this.inputValue);
  634. },
  635. onItemCheckboxChange() {
  636. this.onCommonHandleSaveRecord(this.inputValue);
  637. },
  638. // 下拉框失去焦点处理
  639. onSelectBlur(visible) {
  640. if (!visible) {
  641. this.onCommonHandleSaveRecord(this.inputValue);
  642. }
  643. },
  644. // 统一处理输入变化
  645. onInputChange(val) {
  646. const value = val !== undefined ? val : this.inputValue;
  647. this.$emit('input', value);
  648. this.$emit('change', value);
  649. // 根据输入值判断是否显示错误状态
  650. const isEmpty = this.isValueEmpty(value);
  651. if (this.error && !isEmpty) {
  652. this.$emit('update:error', false);
  653. } else if (!this.error && isEmpty) {
  654. this.$emit('update:error', true);
  655. }
  656. },
  657. // 统一处理失去焦点事件
  658. onBlur(e) {
  659. this.onCommonHandleSaveRecord(e.target.value);
  660. },
  661. // 点击question图标
  662. onClickQuestion() {
  663. const { templateFillType } = this;
  664. if (templateFillType == 'actFill' || templateFillType == 'qc') {
  665. if (templateFillType == 'qc') {
  666. const field = this.getFieldCheckObj()[this.fieldKey];
  667. if (field && field.checked) {
  668. this.$message({
  669. message: '该字段已勾选复核框,请先取消勾选后再进行提交疑问',
  670. type: 'error'
  671. });
  672. return;
  673. }
  674. }
  675. const records = this.getReplyRecords();
  676. let content = "";
  677. if (records.length > 0) {
  678. const o = records[0];
  679. if (!o.reply && templateFillType == 'qc') {//如果填报人员没有回复,qc点击的时候需要回填上次填报的信息
  680. content = o.content;
  681. } else if (templateFillType == 'actFill') {//如果qc没有复核,填报点击的时候需要回填上次填报的信息
  682. content = o.reply;
  683. }
  684. }
  685. this.replyContent = content;
  686. this.visible = true;
  687. }
  688. },
  689. async onCommonHandleSaveRecord(val) {
  690. const isEmpty = this.isValueEmpty(this.inputValue);
  691. if (this.error && !isEmpty) {
  692. this.$emit('update:error', false);
  693. } else if (!this.error && isEmpty) {
  694. this.$emit('update:error', true);
  695. }
  696. // 创建验证控制器,让父组件可以在保存前进行验证
  697. const validationController = {
  698. isPrevented: false,
  699. errorMsg: '',
  700. prevent(msg) {
  701. this.isPrevented = true;
  702. if (msg) this.errorMsg = msg;
  703. }
  704. };
  705. // 触发beforeSaveRecord事件,让父组件进行验证
  706. this.$emit('beforeSaveRecord', {
  707. value: this.inputValue,
  708. oldValue: this.oldValue,
  709. fieldKey: this.fieldKey
  710. }, validationController);
  711. // 检查是否被阻止
  712. if (validationController.isPrevented) {
  713. // 验证不通过,显示错误信息
  714. if (validationController.errorMsg) {
  715. this.$message.error(validationController.errorMsg);
  716. }
  717. // 回退到旧值
  718. this.inputValue = this.oldValue;
  719. this.$emit('input', this.inputValue);
  720. return;
  721. }
  722. if (!this.isFieldsRecord) {//是否需要记录修改记录
  723. this.$emit("blur", this.inputValue);
  724. this.$emit('input', this.inputValue);
  725. this.$emit("change", this.inputValue);
  726. return;
  727. }
  728. // 值发生了变化,需要弹出密码输入框
  729. const isSame = this.isEqual(this.oldValue, this.inputValue);
  730. if (isSame) {
  731. return;
  732. }
  733. if (!this.isValueEmpty(this.oldValue) && !isSame && this.templateFillType === "actFill") {
  734. // 通过EventBus触发电子签名弹窗
  735. EventBus.$emit('showEditSignDialog', { uuid: this.uuid });
  736. } else {//如果是第一次填写,不需要密码验证
  737. this.handleUpdateRecord()
  738. }
  739. },
  740. //如果用户取消,那么回退到上一次的值
  741. resetRecord() {
  742. // 用户点击取消,还原数据
  743. this.inputValue = this.oldValue;
  744. this.$emit('input', this.inputValue); // 触发 v-model 更新
  745. // this.$emit("blur", this.oldValue);
  746. this.$emit("change", this.oldValue, "cancel");
  747. if (this.item.type === "clickable") {
  748. this.$emit("resetRecord");
  749. }
  750. },
  751. //处理更新记录
  752. handleUpdateRecord(data) {
  753. const baseInfo = this.getCommonRecordInfo();
  754. if (!this.oldValue && !this.inputValue) return;
  755. const record = {
  756. ...baseInfo,
  757. oldValue: this.oldValue,
  758. value: this.inputValue,
  759. title: this.oldValue ? "修改" : "提交",
  760. time: moment().format("YYYY-MM-DD HH:mm:ss"),
  761. }
  762. if (data) {
  763. record.reason = data.remark
  764. }
  765. const params = {
  766. type: "fieldChanged",
  767. newRecord: [record],
  768. resourceList: this.getZdxgjl(),
  769. }
  770. //用户输入密码并点击确定,保存修改
  771. this.oldValue = this.inputValue; // 更新旧值
  772. this.$emit("blur", this.inputValue);
  773. this.$emit('input', this.inputValue);
  774. this.$emit("change", this.inputValue, data ? "save" : "");
  775. if (this.item.type === "clickable") {//clickable的丢给父级去处理
  776. return;
  777. }
  778. if (this.templateFillType === "actFill") {//只有实际填报的时候才记录修改记录
  779. this.updateZdxgjl(record);
  780. }
  781. this.$nextTick(()=>{
  782. EventBus.$emit('onModifyRecord', params,)
  783. if (this.regentType.includes(this.item.type)) {
  784. this.$emit("onRegentSubmit", this.selectRegentInfo);
  785. }
  786. })
  787. },
  788. //判断两个值是否相等
  789. isEqual(oldValue, nowValue) {
  790. if (oldValue === null || nowValue === null) {
  791. return oldValue === nowValue;
  792. }
  793. if (typeof oldValue === 'object' && typeof nowValue === 'object') {
  794. return JSON.stringify(oldValue) === JSON.stringify(nowValue);
  795. }
  796. return oldValue === nowValue;
  797. },
  798. //获取公共记录信息
  799. getCommonRecordInfo() {
  800. const { nickName, name } = this.$store.getters;
  801. //locale:zh-CN 中文 en-US 英文
  802. const { label, parentLabel } = this.item;
  803. let fieldLabelCn = this.$i18n.t(label, "zh_CN"), fieldLabelEn = this.$i18n.t(label, "en_US");
  804. if (label === "template.common.other") {
  805. fieldLabelCn = this.$i18n.t(parentLabel, "zh_CN") + this.$i18n.t("template.common.otherInfo", "zh_CN");
  806. fieldLabelEn = this.$i18n.t(parentLabel, "en_US") + this.$i18n.t("template.common.otherInfo", "en_US");
  807. } else if (!label && parentLabel == "template.common.remark") {
  808. fieldLabelCn = this.$i18n.t(parentLabel, "zh_CN") + this.$i18n.t("template.common.unit", "zh_CN");
  809. fieldLabelEn = this.$i18n.t(parentLabel, "en_US") + this.$i18n.t("template.common.unit", "en_US");
  810. }
  811. const commonInfo = {
  812. userNameCn: nickName,
  813. userNameEn: name,
  814. key: this.fieldKey,
  815. fieldCn: `${this.$i18n.t(this.fieldItemLabel, "zh_CN")}` + (fieldLabelCn ? ("-" + fieldLabelCn) : ""),
  816. fieldEn: `${this.$i18n.t(this.fieldItemLabel, "en_US")}` + (fieldLabelEn ? ("-" + fieldLabelEn) : ""),
  817. }
  818. return commonInfo;
  819. },
  820. // 判断值是否为空
  821. isValueEmpty(value) {
  822. if (value === null || value === undefined || value === '') {
  823. return true;
  824. }
  825. if (typeof value === 'string' && value.trim() === '') {
  826. return true;
  827. }
  828. if (Array.isArray(value) && value.length === 0) {
  829. return true;
  830. }
  831. return false;
  832. },
  833. handleClickable(item, event) {
  834. if (this.templateFillType !== 'actFill') {
  835. return
  836. }
  837. this.$emit("clickable", item)
  838. },
  839. //判断是否禁用复选框
  840. getCheckboxDisabled() {
  841. //只有qc能操作checkbox,其他都只能看。
  842. return this.templateFillType !== 'qc'
  843. },
  844. //判断是否显示复选框图标
  845. getIsShowCheckboxIcon() {
  846. if (this.templateFillType === 'qc') {
  847. return true;
  848. }
  849. return this.getChecked();
  850. },
  851. //判断是否显示复制按钮
  852. getIsShowCopyIcon() {
  853. const { copyFrom } = this.item;
  854. return copyFrom && this.templateFillType === "actFill";
  855. },
  856. //判断是否显示操作按钮
  857. isShowHandle() {
  858. const { fillType } = this.item;
  859. //只有当模板状态不是预填时,才显示操作按钮
  860. return this.templateFillType !== "preFill" && fillType === "actFill"
  861. },
  862. //判断是否禁用
  863. getDisabled() {
  864. const { item } = this;
  865. const { fillType } = item;
  866. if (item.hasOwnProperty("disabled")) {
  867. return item.disabled
  868. } else {
  869. if (fillType === "actFill") {//当模板状态是实际填写时,只有当fillType是actFill时才能填写
  870. return this.templateFillType !== "actFill"
  871. } else if (fillType === "preFill") {//当模板状态是预填写时,只有当fillType是preFill才能填写
  872. return this.templateFillType !== "preFill"
  873. } else {
  874. return true
  875. }
  876. }
  877. },
  878. getPlaceholder() {
  879. const { placeholder, label } = this.item;
  880. const { type } = this;
  881. if (this.getDisabled()) {
  882. return ""
  883. }
  884. if (this.regentType.includes(type) || type === "clickable") {
  885. return this.$t("template.common.pleaseSelect")
  886. }
  887. let prex = "template.common.pleaseFillIn"
  888. if (type === "select" || type === "dateTime") {
  889. prex = "template.common.pleaseSelect"
  890. }
  891. return placeholder ? this.$t(placeholder) : (this.$t(prex) + this.$t(label))
  892. },
  893. async onCopy() {
  894. // 触发复制事件
  895. this.$emit("copy");
  896. // 等待复制操作完成后,调用保存记录方法
  897. this.$nextTick(async () => {
  898. await this.onCommonHandleSaveRecord(this.inputValue);
  899. });
  900. },
  901. //判断是否显示问题图标
  902. getIsShowQuestionIcon() {
  903. if (this.templateFillType === "qc") {//qc可以直接查看
  904. return true;
  905. }
  906. return this.getReplyRecords().length > 0;
  907. },
  908. //判断是否显示修改记录图标
  909. getIsShowRecordIcon() {
  910. return this.getModifyRecords().length > 0
  911. },
  912. //获取回复记录
  913. getReplyRecords() {
  914. const { fieldKey, getFhyjjl } = this;
  915. const records = getFhyjjl()?.filter(item => item.key === fieldKey) || [];
  916. return records;
  917. },
  918. //获取字段修改记录
  919. getModifyRecords() {
  920. const { fieldKey, getZdxgjl } = this;
  921. const records = getZdxgjl().filter(item => item.key === fieldKey);
  922. return records;
  923. },
  924. // 鼠标进入主容器
  925. async onMouseEnter(type, event) {
  926. this.currentRecordType = type;
  927. clearTimeout(this.modalTimer);
  928. let record = [];
  929. if (type === "fieldChanged") {
  930. record = this.getModifyRecords();
  931. } else if (type === "replyRecord") {
  932. record = this.getReplyRecords();
  933. }
  934. this.modificationRecords = record;
  935. // 先计算模态框位置,避免闪烁
  936. this.showModal = true;
  937. this.$nextTick(() => {
  938. if (this.$refs.modalRef) {
  939. const elementRect = event.target.getBoundingClientRect();
  940. const modalEl = this.$refs.modalRef;
  941. // 获取模态框的宽度和高度
  942. const modalWidth = modalEl.offsetWidth || 250; // 默认宽度
  943. const modalHeight = modalEl.offsetHeight || 300; // 默认高度
  944. const viewportWidth = window.innerWidth;
  945. const viewportHeight = window.innerHeight;
  946. // 计算模态框位置
  947. let leftPos, topPos;
  948. // 检查右侧空间是否足够
  949. if (elementRect.right + modalWidth + 5 <= viewportWidth) {
  950. // 右侧空间充足,显示在右侧
  951. leftPos = elementRect.right + 5 + 'px';
  952. } else if (elementRect.left - modalWidth - 5 >= 0) {
  953. // 左侧空间充足,显示在左侧
  954. leftPos = elementRect.left - modalWidth - 5 + 'px';
  955. } else {
  956. // 两侧空间都不足,选择空间更大的一边
  957. if (elementRect.left > viewportWidth - elementRect.right) {
  958. // 左侧空间更大,显示在左侧
  959. leftPos = Math.max(5, elementRect.left - modalWidth - 5) + 'px';
  960. } else {
  961. // 右侧空间更大,显示在右侧(可能会超出屏幕,但尽量靠近边缘)
  962. leftPos = Math.min(elementRect.right + 5, viewportWidth - 5) + 'px';
  963. }
  964. }
  965. // 计算顶部位置,确保不超出屏幕上下边界
  966. topPos = Math.max(5, Math.min(elementRect.top, viewportHeight - modalHeight - 5)) + 'px';
  967. // 设置模态框位置
  968. modalEl.style.left = leftPos;
  969. modalEl.style.top = topPos;
  970. }
  971. });
  972. },
  973. // 鼠标离开主容器
  974. onMouseLeave() {
  975. // this.currentRecordType = '';
  976. // this.modificationRecords = [];//清空数据源
  977. // 延迟隐藏模态框,让用户有机会移动到模态框上
  978. this.modalTimer = setTimeout(() => {
  979. if (!this.isHoveringModal) {
  980. this.showModal = false;
  981. }
  982. }, 100);
  983. },
  984. // 鼠标进入模态框
  985. onModalEnter() {
  986. this.isHoveringModal = true;
  987. clearTimeout(this.modalTimer);
  988. },
  989. // 鼠标离开模态框
  990. onModalLeave() {
  991. this.isHoveringModal = false;
  992. this.currentRecordType = "";
  993. this.modificationRecords = [];//清空数据源
  994. this.modalTimer = setTimeout(() => {
  995. this.showModal = false;
  996. }, 100);
  997. },
  998. },
  999. }
  1000. </script>
  1001. <style lang="scss">
  1002. .flex {
  1003. display: flex;
  1004. align-items: center;
  1005. }
  1006. .flex1 {
  1007. flex: 1;
  1008. }
  1009. .handle-row {
  1010. margin-left: 5px;
  1011. display: flex;
  1012. align-items: center;
  1013. cursor: pointer;
  1014. }
  1015. .w-100 {
  1016. width: 100%;
  1017. }
  1018. .handle-icon {
  1019. width: 18px;
  1020. height: 18px;
  1021. &:not(:last-child) {
  1022. margin-right: 5px;
  1023. }
  1024. }
  1025. .mr-5 {
  1026. margin-right: 5px !important;
  1027. }
  1028. .orange {
  1029. color: #f9c588;
  1030. }
  1031. .green {
  1032. color: green;
  1033. }
  1034. .gray {
  1035. color: #b2b2b2;
  1036. }
  1037. .orange-border {
  1038. .el-input-group__prepend,
  1039. input,
  1040. textarea {
  1041. border-color: #f9c588;
  1042. &:focus {
  1043. border-color: #f9c588;
  1044. }
  1045. &:hover {
  1046. border-color: #f9c588;
  1047. }
  1048. &:disabled {
  1049. border-color: #f9c588 !important;
  1050. }
  1051. }
  1052. .el-checkbox__inner {
  1053. border-color: #f9c588 !important;
  1054. }
  1055. }
  1056. .el-button--primary {
  1057. &.orange-border {
  1058. background-color: #f79b31 !important;
  1059. border-color: #f79b31 !important;
  1060. &:hover {
  1061. background-color: #f79b31 !important;
  1062. }
  1063. &:disabled {
  1064. background-color: rgba(#f79b31, .8) !important;
  1065. border-color: rgba(#f79b31, .8) !important;
  1066. }
  1067. &:active {
  1068. background-color: rgba(#f79b31, .8) !important;
  1069. border-color: rgba(#f79b31, .8) !important;
  1070. }
  1071. }
  1072. &.blue-border {
  1073. background-color: #4ea2ff !important;
  1074. border-color: #4ea2ff !important;
  1075. &:hover {
  1076. background-color: #4ea2ff !important;
  1077. }
  1078. &:disabled {
  1079. background-color: rgba(#4ea2ff, .8) !important;
  1080. border-color: rgba(#4ea2ff, .8) !important;
  1081. }
  1082. &:active {
  1083. background-color: rgba(#4ea2ff, .8) !important;
  1084. border-color: rgba(#4ea2ff, .8) !important;
  1085. }
  1086. }
  1087. }
  1088. .green-border {
  1089. .el-input-group__prepend,
  1090. input,
  1091. textarea {
  1092. border-color: green;
  1093. &:focus {
  1094. border-color: green;
  1095. }
  1096. &:hover {
  1097. border-color: green;
  1098. }
  1099. &:disabled {
  1100. border-color: green !important;
  1101. }
  1102. }
  1103. }
  1104. .blue-border {
  1105. .el-input-group__prepend,
  1106. input,
  1107. .el-checkbox__inner,
  1108. textarea {
  1109. border-color: #4ea2ff;
  1110. &:focus {
  1111. border-color: #4ea2ff;
  1112. }
  1113. &:hover {
  1114. border-color: #4ea2ff;
  1115. }
  1116. &:disabled {
  1117. border-color: #4ea2ff !important;
  1118. }
  1119. }
  1120. }
  1121. .error-border {
  1122. .el-input-group__prepend,
  1123. input,
  1124. textarea,
  1125. .el-select,
  1126. .clickable,
  1127. .el-date-editor {
  1128. border-color: #ff5d5d;
  1129. box-shadow: 0 0 6px #ffc3c3 !important;
  1130. &:focus {
  1131. border-color: #ff5d5d;
  1132. box-shadow: 0 0 6px #ffc3c3 !important;
  1133. }
  1134. &:hover {
  1135. border-color: #ff5d5d;
  1136. box-shadow: 0 0 6px #ffc3c3 !important;
  1137. }
  1138. }
  1139. // 为 el-select 和 el-date-picker 添加错误边框样式
  1140. .el-select .el-input__inner,
  1141. .el-date-editor .el-input__inner .el-checkbox__inner {
  1142. border-color: #ff5d5d;
  1143. box-shadow: 0 0 6px #ffc3c3 !important;
  1144. }
  1145. // 处理 DecimalInput 组件的错误边框样式
  1146. :deep(.el-input-number) {
  1147. .el-input__inner {
  1148. border-color: #ff5d5d;
  1149. box-shadow: 0 0 6px #ffc3c3 !important;
  1150. }
  1151. }
  1152. // 为点击式表单项添加错误边框样式
  1153. .clickable {
  1154. border-color: #ff5d5d;
  1155. box-shadow: 0 0 6px #ffc3c3 !important;
  1156. }
  1157. }
  1158. .orange-bg {
  1159. background-color: #FFF1F1 !important; // 橙色背景,透明度适中
  1160. input,
  1161. textarea,
  1162. .el-input__inner,
  1163. .el-textarea__inner {
  1164. background-color: #FFF1F1 !important;
  1165. }
  1166. }
  1167. .modification-modal {
  1168. position: fixed;
  1169. z-index: 9999;
  1170. background-color: rgba(0, 0, 0, 0.7);
  1171. border-radius: 4px;
  1172. padding: 10px;
  1173. color: white;
  1174. max-height: 300px;
  1175. min-width: 250px;
  1176. overflow: hidden;
  1177. pointer-events: auto;
  1178. opacity: 0;
  1179. transform: scale(0.9);
  1180. transition: opacity 0.2s ease, transform 0.2s ease;
  1181. }
  1182. .modification-modal.show {
  1183. opacity: 1;
  1184. transform: scale(1);
  1185. }
  1186. .modification-modal .modal-content {
  1187. max-height: 280px;
  1188. overflow-y: auto;
  1189. padding-right: 5px;
  1190. }
  1191. .modification-modal .modal-content h4 {
  1192. margin: 0 0 10px 0;
  1193. font-size: 14px;
  1194. border-bottom: 1px solid #ccc;
  1195. padding-bottom: 5px;
  1196. }
  1197. .modification-modal .records-list {
  1198. font-size: 12px;
  1199. }
  1200. .modification-modal .record-item p {
  1201. margin: 5px 0;
  1202. word-break: break-all;
  1203. }
  1204. .modification-modal .record-item hr {
  1205. border: 0;
  1206. border-top: 1px solid #555;
  1207. margin: 8px 0;
  1208. }
  1209. .modification-modal .no-records {
  1210. text-align: center;
  1211. color: #aaa;
  1212. font-style: italic;
  1213. }
  1214. .clickable {
  1215. cursor: pointer;
  1216. width: auto;
  1217. // margin-left: 10px;
  1218. min-height: 28px;
  1219. line-height: 28px;
  1220. word-break: break-all;
  1221. border-radius: 4px;
  1222. border: 1px solid #4ea2ff;
  1223. display: flex;
  1224. align-items: center;
  1225. padding: 0 15px;
  1226. font-size: 14px;
  1227. font-weight: normal;
  1228. color: #606266;
  1229. flex: 1;
  1230. &.disabled {
  1231. cursor: not-allowed;
  1232. color: #c0c4cc;
  1233. background-color: #f5f7fa;
  1234. }
  1235. &.error-border {
  1236. border-color: #ff5d5d !important;
  1237. box-shadow: 0 0 6px #ffc3c3 !important;
  1238. }
  1239. }
  1240. .dialog-footer {
  1241. display: flex;
  1242. justify-content: flex-end;
  1243. }
  1244. .atta-tips {
  1245. color: #ff5d5d;
  1246. font-size: 12px;
  1247. margin-left: 5px;
  1248. }
  1249. </style>