华西海圻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.

899 lines
32 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-date-picker v-else-if="type === 'dateTime'" type="datetime" class="flex1"
  24. :class="getFillTypeStyle() + (orangeBg ? ' orange-bg' : '')" v-model="inputValue"
  25. :disabled="getDisabled()" format="yyyy/MM/DD HH:mm:ss" value-format="yyyy/MM/DD HH:mm:ss"
  26. :placeholder="getPlaceholder()" @change="onCommonHandleSaveRecord">
  27. </el-date-picker>
  28. <div class="clickable"
  29. :class="getFillTypeStyle() + (getDisabled() ? ' disabled' : '') + (orangeBg ? ' orange-bg' : '')"
  30. v-else-if="item.type === 'clickable'" @click="handleClickable(item, $event)">
  31. <span v-if="inputValue">{{ inputValue }}</span>
  32. <span v-else class="default-placeholder-text">{{ getPlaceholder() }}</span>
  33. </div>
  34. </div>
  35. <div class="handle-row" v-if="isShowHandle()">
  36. <el-checkbox v-model="checkboxValue" v-if="getIsShowCheckboxIcon()" :disabled="getCheckboxDisabled()" class="mr-5"
  37. @change="onCheckboxChange"></el-checkbox>
  38. <div class="handle-icon" v-if="getIsShowQuestionIcon()" @click="onClickQuestion"
  39. @mouseenter="(e) => onMouseEnter('replyRecord', e)" @mouseleave="onMouseLeave">
  40. <Question :class="getQuestionColor()" />
  41. </div>
  42. <img v-if="getIsShowCopyIcon()" @click="onCopy" src="@/assets/images/copy-icon.svg" class="handle-icon"
  43. alt="" />
  44. <img v-if="getIsShowRecordIcon()" @mouseenter="(e) => onMouseEnter('fieldChanged', e)"
  45. @mouseleave="onMouseLeave" src="@/assets/images/record-icon.svg" class="handle-icon" alt="" />
  46. </div>
  47. <!-- 修改记录模态框 -->
  48. <div v-if="showModal && modificationRecords.length > 0" ref="modalRef"
  49. :class="['modification-modal', { 'show': showModal }]" @mouseenter="onModalEnter"
  50. @mouseleave="onModalLeave">
  51. <div class="modal-content">
  52. <div class="records-list">
  53. <div v-for="(record, index) in modificationRecords" :key="index" class="record-item">
  54. <!-- 字段修改记录 -->
  55. <div class="record-row" v-if="currentRecordType === 'fieldChanged'">
  56. <div>
  57. <span>{{ index + 1 }}.</span>
  58. <span> {{ getUserName(record) }} </span>
  59. <span>{{ record.time }} </span>
  60. <span>{{ modificationRecords.length-1==index?"提交":"修改" }}</span>
  61. </div>
  62. <div v-if = "modificationRecords.length-1!==index">
  63. <div>原值{{ record.oldValue }}</div>
  64. <div>修改值{{ record.value }}</div>
  65. <div v-if="record.reason">备注{{ record.reason }}</div>
  66. </div>
  67. </div>
  68. <!-- 回复记录 -->
  69. <div class="record-row" v-if="currentRecordType === 'replyRecord'">
  70. <div>
  71. <span> {{ getUserName(record) }} </span>
  72. <span>{{ record.time }} </span>
  73. </div>
  74. <div>
  75. <div v-if="record.content">复核意见{{ record.content }}</div>
  76. <div v-if="record.reply">回复意见{{ record.reply }}</div>
  77. </div>
  78. </div>
  79. <hr v-if="index < modificationRecords.length - 1">
  80. </div>
  81. </div>
  82. </div>
  83. </div>
  84. <el-dialog :close-on-click-modal = "false" append-to-body :title="templateFillType == 'actFill' ? '回复意见' : '复核意见'" :visible.sync="visible"
  85. width="30%">
  86. <el-input v-model="replyContent" type="textarea" show-word-limit resize="none" rows="8" placeholder="输入内容"
  87. maxlength="500" />
  88. <span slot="footer" class="dialog-footer">
  89. <el-button @click="visible = false"> </el-button>
  90. <el-button type="primary" @click="onReplyConfirm"> </el-button>
  91. </span>
  92. </el-dialog>
  93. <EditSign @cancel="resetRecord" ref="editSignRef" @callback="onEditSignSave" />
  94. </div>
  95. </template>
  96. <script>
  97. import Question from "./icons/Question.vue";
  98. import DecimalInput from "./DecimalInput.vue";
  99. import { EventBus } from "@/utils/eventBus";
  100. import moment from "moment";
  101. import { deepClone } from "@/utils/index";
  102. import EditSign from "@/views/business/comps/template/dialog/EditSign.vue";
  103. export default {
  104. inject: ['templateFillType', "getZdxgjl", "getFhyjjl", "updateZdxgjl", "replaceFhyjjl", "updateFhyjjl", "getFieldCheckObj", "updateFieldCheckObj"],
  105. components: {
  106. Question,
  107. DecimalInput,
  108. EditSign,
  109. },
  110. props: {
  111. type: {//form类型 input/select等
  112. type: String,
  113. default: "input"
  114. },
  115. item: {
  116. type: Object,
  117. default: () => {
  118. return {
  119. placeholder: "",
  120. maxlength: 30,
  121. label: "",
  122. disabled: false,
  123. }
  124. }
  125. },
  126. // v-model 值
  127. value: {
  128. type: [String, Number, Array],
  129. default: ''
  130. },
  131. // 错误状态
  132. error: {
  133. type: Boolean,
  134. default: false
  135. },
  136. // 橙色背景状态
  137. orangeBg: {
  138. type: Boolean,
  139. default: false
  140. },
  141. fieldKey: {
  142. type: String,
  143. default: ""
  144. },
  145. fieldItemLabel: {
  146. type: String,
  147. default: "",
  148. }
  149. },
  150. data() {
  151. return {
  152. inputValue: this.value,
  153. oldValue: this.value, // 记录上一次的值
  154. showModal: false, // 控制模态框显示
  155. modificationRecords: [], // 存储修改记录
  156. modalTimer: null, // 用于延迟隐藏模态框
  157. isHoveringModal: false, // 是否悬停在模态框上
  158. isHoveringMain: false, // 是否悬停在主元素上(这个实际上不需要,因为我们有事件处理)
  159. currentRecordType: '', // 当前悬停的记录类型(replyRecord 或 modifyRecord)
  160. replyContent: '', // 回复内容
  161. visible: false,//是否显示弹窗
  162. checkboxValue: this.getChecked(),//是否选中
  163. }
  164. },
  165. watch: {
  166. value(newVal) {
  167. this.inputValue = newVal;
  168. // 当type为clickable时,值变化时调用保存记录方法
  169. if (this.item.type === 'clickable') {
  170. this.$nextTick(() => {
  171. this.onCommonHandleSaveRecord(newVal);
  172. });
  173. }
  174. }
  175. },
  176. filters: {
  177. },
  178. mounted() {
  179. },
  180. methods: {
  181. getUserName(record){
  182. const locale = this.$i18n.locale;
  183. if(locale === 'zh_CN'){
  184. return record.userNameCn;
  185. }
  186. return record.userNameEn;
  187. },
  188. onEditSignSave(data) {
  189. this.handleUpdateRecord(data)
  190. },
  191. getChecked() {
  192. return !!this.getFieldCheckObj()[this.fieldKey]?.checked;
  193. },
  194. getFillTypeStyle(type) {
  195. const { fillType } = this.item;
  196. const typeObj = {
  197. actFill: "orange-border",//实际填写的边框颜色
  198. green: "green-border",
  199. preFill: "blue-border",//预填写的边框颜色
  200. }
  201. // 如果有错误状态,返回红色边框样式,覆盖原有的边框颜色
  202. if (this.error) {
  203. return "error-border";
  204. }
  205. return typeObj[fillType] || ""
  206. },
  207. //确认回复
  208. onReplyConfirm() {
  209. if (!this.replyContent) {
  210. this.$message({
  211. message: '请输入内容',
  212. type: 'error'
  213. });
  214. return;
  215. }
  216. const baseInfo = this.getCommonRecordInfo();
  217. const record = {
  218. ...baseInfo,
  219. title: this.templateFillType == 'actFill' ? "回复意见" : "复核意见",
  220. time: moment().format("YYYY-MM-DD HH:mm:ss"),
  221. }
  222. if (this.templateFillType == 'actFill') {
  223. record.reply = this.replyContent;
  224. const deepList = deepClone(this.getFhyjjl());//实际填报应该是修改指定的字段
  225. const item = deepList.find(o => o.key == record.key);
  226. if (item) {
  227. item.reply = this.replyContent;
  228. }
  229. this.replaceFhyjjl(deepList);//实际填报应该是修改指定的字段
  230. } else {
  231. const records = this.getReplyRecords();
  232. record.content = this.replyContent;
  233. if (records.length > 0) {
  234. const o = records[0];
  235. if (o.reply && o.content) {//如果填报人员已回复,那么就产生一条新的记录。
  236. this.updateFhyjjl(record);//qc直接插入数据源
  237. } else {//如果填报人员未填报,只更新当条记录的复核内容
  238. const deepList = deepClone(this.getFhyjjl());
  239. const item = deepList.find(it => it.key == record.key);
  240. if (item) {
  241. item.content = this.replyContent;
  242. }
  243. this.replaceFhyjjl(deepList);
  244. }
  245. } else {
  246. this.updateFhyjjl(record);//qc直接插入数据源
  247. }
  248. }
  249. const params = {
  250. //reply:回复,content:复核
  251. type: this.templateFillType == 'actFill' ? "reply" : "content",
  252. newRecord: record,
  253. resourceList: this.getFhyjjl(),
  254. }
  255. // 触发回复记录事件
  256. EventBus.$emit('onModifyRecord', params);
  257. // 清空回复内容
  258. this.replyContent = '';
  259. // 隐藏弹窗
  260. this.visible = false;
  261. },
  262. //获取question图标颜色
  263. getQuestionColor() {
  264. const records = this.getReplyRecords();
  265. if (records.length > 0) {
  266. const o = records[0];
  267. if (o.reply && o.content) {//有回复意见和复核意见
  268. return "green"
  269. } else if (o.content && !o.reply) {//只有复核意见
  270. return "orange"
  271. } else {
  272. return "gray"
  273. }
  274. } else {//没有回复记录
  275. return "gray"
  276. }
  277. },
  278. // 复选框变化处理
  279. onCheckboxChange(val) {
  280. //有提出意见就不能勾选
  281. if (this.templateFillType == 'qc' && this.getQuestionColor()=== "orange") {
  282. this.checkboxValue = false;
  283. this.$message({
  284. message: '该表单还有质疑项未处理,无法勾选',
  285. type: 'error'
  286. });
  287. return ;
  288. }
  289. this.checkboxValue = val;
  290. // 触发修改记录事件
  291. EventBus.$emit('onModifyRecord', {
  292. type: "checkbox",
  293. fieldCheckObj: JSON.stringify({ ...this.getFieldCheckObj(), [this.fieldKey]: { checked: val } }),//复选框状态对象
  294. });
  295. this.updateFieldCheckObj({ [this.fieldKey]: { checked: val } });
  296. // this.$emit('input', val);
  297. // this.$emit('change', val);
  298. },
  299. onRemoveTag(e) {
  300. this.onCommonHandleSaveRecord(this.inputValue);
  301. },
  302. // 下拉框失去焦点处理
  303. onSelectBlur(visible) {
  304. if (!visible) {
  305. console.log(this.inputValue, "onSelectBlur")
  306. this.onCommonHandleSaveRecord(this.inputValue);
  307. }
  308. },
  309. // 统一处理输入变化
  310. onInputChange(val) {
  311. const value = val !== undefined ? val : this.inputValue;
  312. this.$emit('input', value);
  313. this.$emit('change', value);
  314. // 根据输入值判断是否显示错误状态
  315. const isEmpty = this.isValueEmpty(value);
  316. if (this.error && !isEmpty) {
  317. this.$emit('update:error', false);
  318. } else if (!this.error && isEmpty) {
  319. this.$emit('update:error', true);
  320. }
  321. },
  322. // 统一处理失去焦点事件
  323. onBlur(e) {
  324. this.onCommonHandleSaveRecord(e.target.value);
  325. },
  326. // 点击question图标
  327. onClickQuestion() {
  328. const { templateFillType } = this;
  329. if (templateFillType == 'actFill' || templateFillType == 'qc') {
  330. if (templateFillType == 'qc') {
  331. const field = this.getFieldCheckObj()[this.fieldKey];
  332. if (field && field.checked) {
  333. this.$message({
  334. message: '该字段已勾选复核框,请先取消勾选后再进行提交疑问',
  335. type: 'error'
  336. });
  337. return;
  338. }
  339. }
  340. const records = this.getReplyRecords();
  341. let content = "";
  342. if (records.length > 0) {
  343. const o = records[0];
  344. if (!o.reply && templateFillType == 'qc') {//如果填报人员没有回复,qc点击的时候需要回填上次填报的信息
  345. content = o.content;
  346. } else if (!o.content && templateFillType == 'actFill') {//如果qc没有复核,填报点击的时候需要回填上次填报的信息
  347. content = o.reply;
  348. }
  349. }
  350. this.replyContent = content;
  351. this.visible = true;
  352. }
  353. },
  354. async onCommonHandleSaveRecord(val) {
  355. const isEmpty = this.isValueEmpty(this.inputValue);
  356. if (this.error && !isEmpty) {
  357. this.$emit('update:error', false);
  358. } else if (!this.error && isEmpty) {
  359. this.$emit('update:error', true);
  360. }
  361. console.log(this.oldValue,this.inputValue,"onCommonHandleSaveRecord")
  362. // 值发生了变化,需要弹出密码输入框
  363. console.log(this.oldValue,this.in)
  364. // 值发生了变化,需要弹出密码输入框
  365. const isSame = this.isEqual(this.oldValue, this.inputValue);
  366. if(isSame){
  367. return;
  368. }
  369. if (this.oldValue && !isSame && this.templateFillType === "actFill") {
  370. this.$refs.editSignRef.show()
  371. // this.handleUpdateRecord();
  372. }else{//如果是第一次填写,不需要密码验证
  373. this.handleUpdateRecord()
  374. }
  375. },
  376. //如果用户取消,那么回退到上一次的值
  377. resetRecord() {
  378. // 用户点击取消,还原数据
  379. this.inputValue = this.oldValue;
  380. this.$emit('input', this.inputValue); // 触发 v-model 更新
  381. this.$emit("blur", this.oldValue);
  382. this.$emit("change", this.oldValue);
  383. },
  384. //处理更新记录
  385. handleUpdateRecord(data) {
  386. const baseInfo = this.getCommonRecordInfo();
  387. const record = {
  388. ...baseInfo,
  389. oldValue: this.oldValue,
  390. value: this.inputValue,
  391. title: this.oldValue ? "修改" : "提交",
  392. time: moment().format("YYYY-MM-DD HH:mm:ss"),
  393. }
  394. if (data) {
  395. record.reason = data.remark
  396. }
  397. if(this.templateFillType === "actFill"){//只有实际填报的时候才记录修改记录
  398. this.updateZdxgjl(record);
  399. }
  400. const params = {
  401. type: "fieldChanged",
  402. newRecord: record,
  403. resourceList: this.getZdxgjl(),
  404. }
  405. //用户输入密码并点击确定,保存修改
  406. this.oldValue = this.inputValue; // 更新旧值
  407. this.$emit("blur", this.inputValue);
  408. this.$emit('input', this.inputValue);
  409. this.$emit("change", this.inputValue);
  410. setTimeout(() => {
  411. EventBus.$emit('onModifyRecord', params,)
  412. }, 10);
  413. },
  414. //判断两个值是否相等
  415. isEqual(oldValue, nowValue) {
  416. if (oldValue === null || nowValue === null) {
  417. return oldValue === nowValue;
  418. }
  419. if (typeof oldValue === 'object' && typeof nowValue === 'object') {
  420. return JSON.stringify(oldValue) === JSON.stringify(nowValue);
  421. }
  422. return oldValue === nowValue;
  423. },
  424. //获取公共记录信息
  425. getCommonRecordInfo() {
  426. const { nickName, name } = this.$store.getters;
  427. //locale:zh-CN 中文 en-US 英文
  428. const lang = this.$i18n.locale === "zh_CN" ? "cn" : "en";
  429. const { label, parentLabel } = this.item;
  430. let fieldLabelCn = this.$i18n.t(label,"zh_CN"),fieldLabelEn = this.$i18n.t(label,"en_US");
  431. if (label === "template.common.other") {
  432. fieldLabelCn = this.$i18n.t(parentLabel,"zh_CN")+this.$i18n.t("template.common.otherInfo","zh_CN");
  433. fieldLabelEn = this.$i18n.t(parentLabel,"en_US")+this.$i18n.t("template.common.otherInfo","en_US");
  434. } else if (!label && parentLabel == "template.common.remark") {
  435. fieldLabelCn = this.$i18n.t(parentLabel,"zh_CN")+this.$i18n.t("template.common.unit","zh_CN");
  436. fieldLabelEn = this.$i18n.t(parentLabel,"en_US")+this.$i18n.t("template.common.unit","en_US");
  437. }
  438. const commonInfo = {
  439. userNameCn: nickName,
  440. userNameEn: name,
  441. key: this.fieldKey,
  442. fieldCn: `${this.$i18n.t(this.fieldItemLabel,"zh_CN")}`+(fieldLabelCn?("-"+fieldLabelCn):""),
  443. fieldEn: `${this.$i18n.t(this.fieldItemLabel,"en_US")}`+(fieldLabelEn?("-"+fieldLabelEn):""),
  444. }
  445. return commonInfo;
  446. },
  447. // 判断值是否为空
  448. isValueEmpty(value) {
  449. if (value === null || value === undefined || value === '') {
  450. return true;
  451. }
  452. if (typeof value === 'string' && value.trim() === '') {
  453. return true;
  454. }
  455. if (Array.isArray(value) && value.length === 0) {
  456. return true;
  457. }
  458. return false;
  459. },
  460. handleClickable(item, event) {
  461. if (item.fillType !== 'actFill') {
  462. return
  463. }
  464. this.$emit("clickable", item)
  465. },
  466. //判断是否禁用复选框
  467. getCheckboxDisabled(){
  468. //只有qc能操作checkbox,其他都只能看。
  469. return this.templateFillType !== 'qc'
  470. },
  471. //判断是否显示复选框图标
  472. getIsShowCheckboxIcon() {
  473. if(this.templateFillType === 'qc'){
  474. return true;
  475. }
  476. return this.getChecked();
  477. },
  478. //判断是否显示复制按钮
  479. getIsShowCopyIcon() {
  480. const { copyFrom } = this.item;
  481. return copyFrom && this.templateFillType === "actFill";
  482. },
  483. //判断是否显示操作按钮
  484. isShowHandle() {
  485. const { fillType } = this.item;
  486. //只有当模板状态不是预填时,才显示操作按钮
  487. return this.templateFillType !== "preFill" && fillType === "actFill"
  488. },
  489. //判断是否禁用
  490. getDisabled() {
  491. const { item } = this;
  492. const { fillType } = item;
  493. if (item.hasOwnProperty("disabled")) {
  494. return item.disabled
  495. } else {
  496. if (fillType === "actFill") {//当模板状态是实际填写时,只有当fillType是actFill时才能填写
  497. return this.templateFillType !== "actFill"
  498. } else if (fillType === "preFill") {//当模板状态是预填写时,只有当fillType是preFill才能填写
  499. return this.templateFillType !== "preFill"
  500. } else {
  501. return true
  502. }
  503. }
  504. },
  505. getPlaceholder() {
  506. const { placeholder, label } = this.item;
  507. const { type } = this;
  508. if (this.getDisabled()) {
  509. return ""
  510. }
  511. if (type === "clickable") {
  512. return this.$t("template.common.pleaseSelect")
  513. }
  514. let prex = "template.common.pleaseFillIn"
  515. if (type === "select" || type === "dateTime") {
  516. prex = "template.common.pleaseSelect"
  517. }
  518. return placeholder ? this.$t(placeholder) : (this.$t(prex) + this.$t(label))
  519. },
  520. async onCopy() {
  521. // 触发复制事件
  522. this.$emit("copy");
  523. // 等待复制操作完成后,调用保存记录方法
  524. this.$nextTick(async () => {
  525. await this.onCommonHandleSaveRecord(this.inputValue);
  526. });
  527. },
  528. //判断是否显示问题图标
  529. getIsShowQuestionIcon() {
  530. if (this.templateFillType === "qc") {//qc可以直接查看
  531. return true;
  532. }
  533. return this.getReplyRecords().length > 0;
  534. },
  535. //判断是否显示修改记录图标
  536. getIsShowRecordIcon() {
  537. return this.getModifyRecords().length > 0
  538. },
  539. //获取回复记录
  540. getReplyRecords() {
  541. const { fieldKey, getFhyjjl } = this;
  542. const records = getFhyjjl()?.filter(item => item.key === fieldKey) || [];
  543. return records;
  544. },
  545. //获取字段修改记录
  546. getModifyRecords() {
  547. const { fieldKey, getZdxgjl } = this;
  548. const records = getZdxgjl().filter(item => item.key === fieldKey);
  549. return records;
  550. },
  551. // 鼠标进入主容器
  552. async onMouseEnter(type, event) {
  553. this.currentRecordType = type;
  554. clearTimeout(this.modalTimer);
  555. let record = [];
  556. if (type === "fieldChanged") {
  557. record = this.getModifyRecords();
  558. } else if (type === "replyRecord") {
  559. record = this.getReplyRecords();
  560. }
  561. this.modificationRecords = record;
  562. // 先计算模态框位置,避免闪烁
  563. this.showModal = true;
  564. this.$nextTick(() => {
  565. if (this.$refs.modalRef) {
  566. const elementRect = event.target.getBoundingClientRect();
  567. const modalEl = this.$refs.modalRef;
  568. // 获取模态框的宽度和高度
  569. const modalWidth = modalEl.offsetWidth || 250; // 默认宽度
  570. const modalHeight = modalEl.offsetHeight || 300; // 默认高度
  571. const viewportWidth = window.innerWidth;
  572. const viewportHeight = window.innerHeight;
  573. // 计算模态框位置
  574. let leftPos, topPos;
  575. // 检查右侧空间是否足够
  576. if (elementRect.right + modalWidth + 5 <= viewportWidth) {
  577. // 右侧空间充足,显示在右侧
  578. leftPos = elementRect.right + 5 + 'px';
  579. } else if (elementRect.left - modalWidth - 5 >= 0) {
  580. // 左侧空间充足,显示在左侧
  581. leftPos = elementRect.left - modalWidth - 5 + 'px';
  582. } else {
  583. // 两侧空间都不足,选择空间更大的一边
  584. if (elementRect.left > viewportWidth - elementRect.right) {
  585. // 左侧空间更大,显示在左侧
  586. leftPos = Math.max(5, elementRect.left - modalWidth - 5) + 'px';
  587. } else {
  588. // 右侧空间更大,显示在右侧(可能会超出屏幕,但尽量靠近边缘)
  589. leftPos = Math.min(elementRect.right + 5, viewportWidth - 5) + 'px';
  590. }
  591. }
  592. // 计算顶部位置,确保不超出屏幕上下边界
  593. topPos = Math.max(5, Math.min(elementRect.top, viewportHeight - modalHeight - 5)) + 'px';
  594. // 设置模态框位置
  595. modalEl.style.left = leftPos;
  596. modalEl.style.top = topPos;
  597. }
  598. });
  599. },
  600. // 鼠标离开主容器
  601. onMouseLeave() {
  602. // this.currentRecordType = '';
  603. // this.modificationRecords = [];//清空数据源
  604. // 延迟隐藏模态框,让用户有机会移动到模态框上
  605. this.modalTimer = setTimeout(() => {
  606. if (!this.isHoveringModal) {
  607. this.showModal = false;
  608. }
  609. }, 100);
  610. },
  611. // 鼠标进入模态框
  612. onModalEnter() {
  613. this.isHoveringModal = true;
  614. clearTimeout(this.modalTimer);
  615. },
  616. // 鼠标离开模态框
  617. onModalLeave() {
  618. this.isHoveringModal = false;
  619. this.currentRecordType = "";
  620. this.modificationRecords = [];//清空数据源
  621. this.modalTimer = setTimeout(() => {
  622. this.showModal = false;
  623. }, 100);
  624. },
  625. },
  626. }
  627. </script>
  628. <style lang="scss">
  629. .flex {
  630. display: flex;
  631. align-items: center;
  632. }
  633. .flex1 {
  634. flex: 1;
  635. }
  636. .handle-row {
  637. margin-left: 10px;
  638. display: flex;
  639. align-items: center;
  640. cursor: pointer;
  641. }
  642. .w-100 {
  643. width: 100%;
  644. }
  645. .handle-icon {
  646. width: 18px;
  647. height: 18px;
  648. &:not(:last-child) {
  649. margin-right: 5px;
  650. }
  651. }
  652. .mr-5 {
  653. margin-right: 5px !important;
  654. }
  655. .orange {
  656. color: #f9c588;
  657. }
  658. .green {
  659. color: green;
  660. }
  661. .gray {
  662. color: #b2b2b2;
  663. }
  664. .orange-border {
  665. .el-input-group__prepend,
  666. input,
  667. textarea {
  668. border-color: #f9c588;
  669. &:focus {
  670. border-color: #f9c588;
  671. }
  672. &:hover {
  673. border-color: #f9c588;
  674. }
  675. &:disabled {
  676. border-color: #f9c588 !important;
  677. }
  678. }
  679. }
  680. .green-border {
  681. .el-input-group__prepend,
  682. input,
  683. textarea {
  684. border-color: green;
  685. &:focus {
  686. border-color: green;
  687. }
  688. &:hover {
  689. border-color: green;
  690. }
  691. &:disabled {
  692. border-color: green !important;
  693. }
  694. }
  695. }
  696. .blue-border {
  697. .el-input-group__prepend,
  698. input,
  699. textarea {
  700. border-color: #4ea2ff;
  701. &:focus {
  702. border-color: #4ea2ff;
  703. }
  704. &:hover {
  705. border-color: #4ea2ff;
  706. }
  707. &:disabled {
  708. border-color: #4ea2ff !important;
  709. }
  710. }
  711. }
  712. .error-border {
  713. .el-input-group__prepend,
  714. input,
  715. textarea,
  716. .el-select,
  717. .clickable,
  718. .el-date-editor {
  719. border-color: #f56c6c;
  720. &:focus {
  721. border-color: #f56c6c;
  722. }
  723. &:hover {
  724. border-color: #f56c6c;
  725. }
  726. }
  727. // 为 el-select 和 el-date-picker 添加错误边框样式
  728. .el-select .el-input__inner,
  729. .el-date-editor .el-input__inner {
  730. border-color: #f56c6c;
  731. }
  732. // 处理 DecimalInput 组件的错误边框样式
  733. :deep(.el-input-number) {
  734. .el-input__inner {
  735. border-color: #f56c6c;
  736. }
  737. }
  738. // 为点击式表单项添加错误边框样式
  739. .clickable {
  740. border-color: #f56c6c;
  741. }
  742. }
  743. .orange-bg {
  744. background-color: #FFF1F1 !important; // 橙色背景,透明度适中
  745. input,
  746. textarea,
  747. .el-input__inner,
  748. .el-textarea__inner {
  749. background-color: #FFF1F1 !important;
  750. }
  751. }
  752. .modification-modal {
  753. position: fixed;
  754. z-index: 9999;
  755. background-color: rgba(0, 0, 0, 0.7);
  756. border-radius: 4px;
  757. padding: 10px;
  758. color: white;
  759. max-height: 300px;
  760. min-width: 250px;
  761. overflow: hidden;
  762. pointer-events: auto;
  763. opacity: 0;
  764. transform: scale(0.9);
  765. transition: opacity 0.2s ease, transform 0.2s ease;
  766. }
  767. .modification-modal.show {
  768. opacity: 1;
  769. transform: scale(1);
  770. }
  771. .modification-modal .modal-content {
  772. max-height: 280px;
  773. overflow-y: auto;
  774. padding-right: 5px;
  775. }
  776. .modification-modal .modal-content h4 {
  777. margin: 0 0 10px 0;
  778. font-size: 14px;
  779. border-bottom: 1px solid #ccc;
  780. padding-bottom: 5px;
  781. }
  782. .modification-modal .records-list {
  783. font-size: 12px;
  784. }
  785. .modification-modal .record-item p {
  786. margin: 5px 0;
  787. word-break: break-all;
  788. }
  789. .modification-modal .record-item hr {
  790. border: 0;
  791. border-top: 1px solid #555;
  792. margin: 8px 0;
  793. }
  794. .modification-modal .no-records {
  795. text-align: center;
  796. color: #aaa;
  797. font-style: italic;
  798. }
  799. .clickable {
  800. cursor: pointer;
  801. width: auto;
  802. // margin-left: 10px;
  803. min-width: 178px;
  804. height: 28px;
  805. border-radius: 4px;
  806. border: 1px solid #4ea2ff;
  807. display: flex;
  808. align-items: center;
  809. padding: 0 15px;
  810. font-size: 14px;
  811. font-weight: normal;
  812. color: #606266;
  813. flex: 1;
  814. &.disabled {
  815. cursor: not-allowed;
  816. color: #c0c4cc;
  817. background-color: #f5f7fa;
  818. }
  819. &.error-border {
  820. border-color: #f56c6c !important;
  821. }
  822. }
  823. .dialog-footer {
  824. display: flex;
  825. justify-content: flex-end;
  826. }
  827. </style>