Procházet zdrojové kódy

Merge branch 'dev' of http://110.41.163.243:9980/kd-aiot/kd-aiot-frontend into dengfei

695593266@qq.com před 1 měsícem
rodič
revize
5acba2d351

+ 155 - 86
src/components/upload/WithView.vue

@@ -1,113 +1,182 @@
 <template>
   <div>
+    <!-- 图片预览 -->
     <div class="img-view" v-if="dialogImageUrl">
       <img :src="dialogImageUrl" alt="" srcset="" />
     </div>
     <div class="placeholder-box" v-else>
       <img src="~@/assets/upload-placeholder.svg" alt="" />
+      <span>请上传签名</span>
     </div>
+
+    <!-- 操作按钮 -->
     <div class="btn-box">
-      <el-upload class="avatar-div" action="#" accept="image/png,image/jpeg" :show-file-list="false" ref="uploadRef"
-        :on-exceed="handleExceed" :limit="1" :http-request="handlSuccess" :multiple="false">
-        <el-button type="text">上传{{ assetName }}图片</el-button>
-      </el-upload>
+      <el-button type="text" @click="triggerFileInput" :loading="uploading">
+        上传{{ assetName }}图片
+      </el-button>
       <el-button type="text" @click="clearImg">清除图片</el-button>
     </div>
+
+    <!-- 隐藏的文件选择器 -->
+    <input
+      type="file"
+      ref="fileInput"
+      accept="image/png,image/jpeg"
+      style="display: none"
+      @change="handleFileChange"
+    />
+
+    <!-- ele-cropper-modal 裁剪弹窗 -->
+    <EleCropperDialog
+      :show.sync="showCropper"
+      :src="cropperSrc"
+      :aspect-ratio="1 / 1"
+      append-to-body
+      image-type="image/png"
+      :to-blob="true"
+      @crop="handleCropSuccess"
+    />
   </div>
 </template>
 
 <script>
-import { uploadFile, removeFile } from '@/api/system/file/index.js';
-import { getImageUrl } from '@/utils/file';
-export default {
-  props: {
-    assetName: {
-      type: String,
-      default: '设备'
+  import { uploadFile, removeFile } from '@/api/system/file/index.js';
+  import { getImageUrl } from '@/utils/file';
+
+  // 如果 ele-cropper-modal 没有全局注册,需要在此处导入
+  import EleCropperDialog from 'ele-admin/es/ele-cropper-dialog';
+
+  export default {
+    // 如果未全局注册,取消下面注释
+    components: { EleCropperDialog },
+    props: {
+      assetName: {
+        type: String,
+        default: '设备'
+      },
+      value: {
+        type: Object,
+        default: () => ({})
+      },
+      // 所属模块
+      module: {
+        type: String,
+        default: 'main'
+      }
     },
-    value: {
-      type: Object,
-      default: () => []
+    data() {
+      return {
+        showCropper: false, // 控制裁剪弹窗显示
+        cropperSrc: null, // 待裁剪图片的临时地址
+        uploading: false // 上传状态
+      };
     },
-    // 所属模块
-    module: {
-      type: String,
-      default: 'main'
-    }
-  },
-  data() {
-    return {};
-  },
-  computed: {
-    dialogImageUrl() {
-      return this.value?.storePath && getImageUrl(this.value.storePath);
-    }
-  },
-  methods: {
-    // 清空已上传的文件列表
-    clearUploadFiles() { },
-    //图片添加
-    async handlSuccess(params) {
-      let res = await uploadFile({
-        multiPartFile: params.file,
-        module: this.module
-      });
-      if (res?.data) {
-        this.$emit('input', res.data);
+    computed: {
+      dialogImageUrl() {
+        return this.value?.storePath && getImageUrl(this.value.storePath);
       }
     },
-    async clearImg() {
-      if(!this.value.id){
-          return
+    methods: {
+      // 触发隐藏的文件选择器
+      triggerFileInput() {
+        this.$refs.fileInput.click();
+      },
+
+      // 文件选择后的处理
+      handleFileChange(event) {
+        const file = event.target.files[0];
+        if (!file) return;
+
+        // 读取文件为 Data URL,用作裁剪器的图片源
+        const reader = new FileReader();
+        reader.onload = (e) => {
+          this.cropperSrc = e.target.result;
+          this.showCropper = true;
+          // 重置 input,允许重复选择同一文件
+          event.target.value = '';
+        };
+        reader.readAsDataURL(file);
+      },
+
+      // 裁剪成功回调
+      async handleCropSuccess(blob) {
+        // 因为设置了 :to-blob="true",所以直接拿到 Blob 对象
+        if (!blob) {
+          this.showCropper = false;
+          return;
+        }
+
+        this.uploading = true;
+        try {
+          const file = new File([blob], 'signature.png', { type: 'image/png' });
+          // const formData = new FormData();
+          // formData.append('multiPartFile', file);
+          // formData.append('module', this.module);
+
+          const res = await uploadFile({
+            multiPartFile: file,
+            module: this.module
+          });
+          if (res?.data) {
+            this.$emit('input', res.data);
+            this.$message.success('上传成功');
+          }
+        } catch (error) {
+          console.error(error);
+          this.$message.error('上传失败,请稍后重试');
+        } finally {
+          this.uploading = false;
+          this.showCropper = false;
+        }
+      },
+
+      // 清除图片
+      async clearImg() {
+        if (!this.value.id) return;
+        try {
+          await removeFile({ fileId: this.value.id });
+          this.$emit('input', {});
+          this.$message.success('已清除');
+        } catch (error) {
+          this.$message.error('清除失败');
+        }
       }
-      await removeFile({ fileId: this.value.id });
-      this.$emit('input', {});
-      this.$refs.uploadRef.clearFiles();
-    },
-    // 限制上传的数量
-    handleExceed(files, fileList) {
-      this.$message.warning(`最多允许上传一张图片!`);
     }
-  }
-};
+  };
 </script>
+
 <style lang="scss" scoped>
-.img-view {
-  width: 280px;
-  height: 342px;
-  display: flex;
-  justify-content: center;
-  align-items: center;
-  border-width: 1px;
-  border-style: solid;
-  border-color: rgba(215, 215, 215, 1);
-
-  img {
-    max-width: 100%;
-    max-height: 100%;
+  .img-view {
+    width: 280px;
+    height: 342px;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    border: 1px solid #d7d7d7;
+
+    img {
+      max-width: 100%;
+      max-height: 100%;
+    }
   }
-}
-
-.placeholder-box {
-  width: 250px;
-  height: 200px;
-  text-align: center;
-
-  background-color: rgba(242, 242, 242, 1);
-  box-sizing: border-box;
-  border-width: 1px;
-  border-style: solid;
-  border-color: rgba(215, 215, 215, 1);
-  padding-top: 60px;
-
-  img {
-    width: 100px;
-    height: 100px;
+
+  .placeholder-box {
+    width: 250px;
+    height: 200px;
+    text-align: center;
+    background-color: #f2f2f2;
+    box-sizing: border-box;
+    border: 1px solid #d7d7d7;
+    padding-top: 60px;
+
+    img {
+      width: 100px;
+      height: 100px;
+    }
   }
-}
 
-.btn-box {
-  display: flex;
-  justify-content: space-around;
-}
+  .btn-box {
+    display: flex;
+    justify-content: space-around;
+  }
 </style>

+ 1 - 1
src/views/factoryModel/qualificationManagement/components/vendorDialog.vue

@@ -291,7 +291,7 @@
           ...where
         });
       },
-      open(ids) {
+      open(ids=[]) {
         this.visible = true;
         this.disabledId = ids;
         this.$nextTick(() => {

+ 568 - 0
src/views/rulesManagement/releaseRules/components/experimentationProcessNew.vue

@@ -0,0 +1,568 @@
+<template>
+  <div class="ele-body">
+    <el-button type="primary" @click="addHtml('customText')" v-if="edit"
+      >插入自定义文本</el-button
+    >
+    <el-button type="primary" @click="addHtml('customTable')" v-if="edit"
+      >插入表格</el-button
+    >
+    <!-- <el-button type="primary" @click="save()">保存</el-button> -->
+    <div
+      style="
+        display: flex;
+        width: 100%;
+        padding: 10px;
+        border: solid 1px #f1f1f1;
+        margin-top: 10px;
+      "
+    >
+      <vue-draggable
+        v-model="list"
+        group="project1"
+        :animation="300"
+        handle=".sort-handle"
+        style="flex: 1;max-height:calc(100vh - 500px);overflow: auto;"
+      >
+        <div
+          class="demo-drag-list-item ele-cell"
+          v-for="item in list"
+          :key="item.id"
+        >
+          <div class="listItem">
+            <i
+              class="sort-handle el-icon-_nav move"
+              style="display: none"
+              v-if="edit"
+            ></i>
+            <i
+              class="sort-handle el-icon-delete delete"
+              v-if="edit"
+              style="display: none"
+              @click="del(item.id)"
+            ></i>
+            <customText
+              :ref="'customTextRef' + item.id"
+              style="flex: 1"
+              v-if="item.type == 'customText'"
+              :id="item.id"
+              :form="item.value"
+              :valueObj="item.valueObj"
+              @editShow="editShowFn"
+              @calculation="calculation"
+              :edit="edit"
+              :readonly="readonly"
+            ></customText>
+            <customTable
+              :ref="'customTextRef' + item.id"
+              style="flex: 1"
+              v-if="item.type == 'customTable'"
+              :id="item.id"
+              :form="item.value"
+              :valueObj="item.valueObj"
+              @calculation="calculation"
+              @editShow="editShowFn"
+              :readonly="readonly"
+              @copy="copy"
+              :edit="edit"
+            ></customTable>
+          </div>
+        </div>
+      </vue-draggable>
+
+      <el-card class="box-card" v-show="editShow" style="width: 320px">
+        <div slot="header" class="clearfix">
+          <span>配置</span>
+          <el-button
+            style="float: right; padding: 3px 0"
+            type="text"
+            @click="editShow = false"
+            >关闭</el-button
+          >
+        </div>
+        <el-form label-width="80px">
+          <el-form-item label="字段标识:" prop="id">
+            <el-input
+              v-model="domObj.id"
+              placeholder=""
+              @change="editInputChange"
+            ></el-input>
+          </el-form-item>
+          <el-form-item label="宽度:" prop="width" v-if="!domObj.isNoWidth">
+            <el-input
+              v-model="domObj.width"
+              type="number"
+              placeholder=""
+              @change="editInputChange"
+            ></el-input>
+          </el-form-item>
+          <el-form-item label="是否只读:" prop="readonly">
+            <el-select
+              v-model="domObj.readonly"
+              placeholder="请选择"
+              @change="editInputChange"
+            >
+              <el-option :key="1" label="否" :value="1"> </el-option>
+              <el-option :key="2" label="是" :value="2"> </el-option>
+            </el-select>
+          </el-form-item>
+          <el-form-item label="计算公式:" prop="readonly">
+            <el-input
+              :value="domObj.equation?.map((item) => item.value).join('')"
+              type="textarea"
+              placeholder=""
+              disabled
+            ></el-input>
+            <el-button type="primary" @click="setEquation">配置公式</el-button>
+            <el-button type="primary" @click="delEquation">重置</el-button>
+          </el-form-item>
+        </el-form>
+      </el-card>
+    </div>
+    <ele-modal
+      title="配置公式"
+      :visible.sync="visible"
+      :close-on-click-modal="false"
+      append-to-body
+      width="800px"
+      resizable
+      maxable
+    >
+      <div class="formula-builder__selects">
+        <!-- 选择参数:从已填的非计算参数内容里取 -->
+        <el-select
+          v-model="equationUnit.paramSelect"
+          placeholder="选择参数"
+          size="mini"
+          style="width: 100px; margin-right: 8px; flex-shrink: 0"
+          @change="paramSelectChange($event, 'id')"
+          filterable
+        >
+          <el-option
+            v-for="item in idList"
+            :key="item"
+            :label="item"
+            :value="item"
+          />
+        </el-select>
+
+        <!-- 选择运算符 -->
+        <el-select
+          v-model="equationUnit.opSelect"
+          placeholder="选择符号"
+          size="mini"
+          style="width: 100px; flex-shrink: 0; margin-right: 8px"
+          @change="paramSelectChange($event, 'symbol')"
+        >
+          <el-option
+            v-for="op in opSelectOptions"
+            :key="op"
+            :label="op"
+            :value="op"
+          />
+        </el-select>
+        <!-- 选择值 -->
+        <el-input
+          v-model="equationUnit.currentValue"
+          placeholder="输入值"
+          size="mini"
+          style="width: 150px; flex-shrink: 0"
+        >
+          <template slot="append">
+            <span
+              style="cursor: pointer"
+              @click="paramSelectChange(equationUnit.currentValue, 'value')"
+              >确认</span
+            >
+          </template>
+        </el-input>
+        <!-- 替换或者追加 -->
+        <el-select
+          v-if="equationUnit.activeIndex != undefined"
+          v-model="equationUnit.replaceOrAppend"
+          placeholder="选择"
+          size="mini"
+          style="width: 80px; margin-left: 8px; flex-shrink: 0"
+        >
+          <el-option key="append" label="追加" value="append" />
+          <el-option key="replace" label="替换" value="replace" />
+        </el-select>
+        <el-input
+          v-model.number="domObj.units.decimalPlace"
+          placeholder="小数位"
+          size="mini"
+          style="width: 120px; margin-left: 8px; flex-shrink: 0"
+        >
+        </el-input>
+        <el-select
+          v-model="domObj.units.takeValueMethod"
+          placeholder="取值方法"
+          size="mini"
+          style="width: 100px; margin-left: 8px; flex-shrink: 0"
+        >
+          <el-option key="1" label="四舍五入" value="1" />
+          <el-option key="2" label="去尾" value="2" />
+        </el-select>
+      </div>
+
+      <!-- 已组装公式标签展示 -->
+      <div
+        v-if="equationUnit.equation.length"
+        style="
+          display: inline-flex;
+          flex-wrap: wrap;
+          max-width: 100%;
+          margin-top: 5px;
+        "
+      >
+        <el-tag
+          v-for="(p, index) in equationUnit.equation"
+          :key="index"
+          size="mini"
+          closable
+          :type="equationUnit.activeIndex === index ? 'primary' : 'info'"
+          @click="formulaPartsTagClick(index)"
+          @close="tagItemDelete(index)"
+        >
+          {{ p.value }}
+        </el-tag>
+      </div>
+
+      <el-input
+        style="margin-top: 5px"
+        :value="equationUnit.equation?.map((item) => item.value).join('')"
+        type="textarea"
+        placeholder=""
+        disabled
+      ></el-input>
+      <div slot="footer" class="footer">
+        <el-button type="primary" @click="editInputChange('equation')"
+          >确认</el-button
+        >
+        <el-button @click="visible = false">返回</el-button>
+      </div>
+    </ele-modal>
+  </div>
+</template>
+
+<script>
+  import customText from '../components/templateDiv/customText.vue';
+  import customTable from '../components/templateDiv/customTableNew.vue';
+  import VueDraggable from 'vuedraggable';
+  import { generateRandomString } from '@/utils/util';
+
+  export default {
+    components: {
+      customText,
+      VueDraggable,
+      customTable
+    },
+    props: {
+      edit: {
+        default: true,
+        type: Boolean
+      },
+      readonly: {
+        default: false,
+        type: Boolean
+      }
+    },
+    computed: {},
+    data() {
+      return {
+        list: [],
+        editShow: false,
+        visible: false,
+        templateDivRef: '',
+        domObj: { units: {} },
+        idList: [],
+        opSelectOptions: ['+', '-', '*', '/', '%', '(', ')'],
+        equationUnit: {
+          equation: [],
+          activeIndex: '',
+          paramSelect: '',
+          opSelect: '',
+          currentValue: '',
+          replaceOrAppend: 'append'
+        }
+      };
+    },
+    mounted() {},
+    created() {},
+    methods: {
+      // 计算
+      calculation() {
+        this.getValue();
+        let equation = [];
+
+        this.list.forEach((item) => {
+          equation.push({
+            id: item.id,
+            equation: item.equation
+          });
+        });
+        equation.forEach((item) => {
+          for (const key in item.equation) {
+            let { data, units } = this.getObjValue(); //每次计算都获取最新的值
+            let value = '';
+            if (item.equation[key].length) {
+              item.equation[key].forEach((equationItem) => {
+                if (
+                  equationItem.type == 'symbol' ||
+                  equationItem.type == 'value'
+                ) {
+                  value += equationItem.value;
+                } else if (equationItem.type == 'id') {
+                  value += Number(data[equationItem.value]) || 0;
+                }
+              });
+              if (units[key]?.decimalPlace) {
+                if (units[key]?.takeValueMethod) {
+                  value =
+                    units[key]?.takeValueMethod == 1
+                      ? parseFloat(eval(value).toFixed(units[key].decimalPlace))
+                      : this.truncateToFixedManual(
+                          eval(value),
+                          units[key].decimalPlace
+                        );
+                } else {
+                  value = parseFloat(
+                    eval(value).toFixed(units[key].decimalPlace)
+                  );
+                }
+              } else {
+                value = parseFloat(eval(value).toFixed(2));
+              }
+              if (this.$refs['customTextRef' + item.id][0]) {
+                this.$refs['customTextRef' + item.id][0].equationValue({
+                  domId: key,
+                  value
+                });
+              }
+            }
+          }
+        });
+      },
+
+      truncateToFixedManual(num, decimalPlaces) {
+        let factor = Math.pow(10, decimalPlaces);
+        return Math.floor(num * factor) / factor;
+      },
+      getObjValue() {
+        this.getValue();
+        let data = {};
+        let units = {};
+        this.list.forEach((item) => {
+          units = { ...item.units, ...units };
+
+          if (item.type == 'customText') {
+            data = { ...item.valueObj, ...data };
+          } else {
+            item.valueObj.tableData.forEach((row) => {
+              row.forEach((cell) => {
+                data[cell.id] = cell.value;
+              });
+            });
+          }
+        });
+        return { data: data || {}, units: units || {} };
+      },
+
+      setEquation() {
+        this.getValue();
+        this.idList = [];
+        if (this.domObj.equation) {
+          this.equationUnit.equation = JSON.parse(
+            JSON.stringify(this.domObj.equation)
+          );
+          this.equationUnit.activeIndex = this.domObj.equation.length;
+        } else {
+          this.equationUnit.equation = [];
+        }
+        this.list.forEach((item) => {
+          if (item.type == 'customText') {
+            for (let key in item.valueObj) {
+              this.idList.push(key);
+            }
+          } else {
+            item.valueObj.tableData.forEach((row) => {
+              row.forEach((cell) => {
+                this.idList.push(cell.id);
+              });
+            });
+          }
+        });
+        this.visible = true;
+      },
+      delEquation() {
+        this.equationUnit.equation = [];
+        this.editInputChange('equation');
+      },
+      tagItemDelete(index) {
+        this.equationUnit.equation.splice(index, 1);
+      },
+      formulaPartsTagClick(index, row) {
+        if (!this.equationUnit.replaceOrAppend) {
+          // 默认追加
+          this.equationUnit_replaceOrAppend = 'append';
+        }
+
+        if (
+          this.equationUnit.activeIndex &&
+          this.equationUnit.activeIndex === index
+        ) {
+          this.$set(this.equationUnit, 'activeIndex', undefined);
+        } else {
+          this.$set(this.equationUnit, 'activeIndex', index);
+        }
+      },
+      paramSelectChange(val, type) {
+        if (!val) {
+          return;
+        }
+        if (type == 'id') {
+          this.setValue({ type: 'id', value: val });
+          this.equationUnit.paramSelect = null;
+        } else if (type == 'symbol') {
+          this.setValue({ type: 'symbol', value: val });
+
+          this.equationUnit.opSelect = null;
+        } else if (type == 'value') {
+          this.setValue({ type: 'value', value: val });
+        }
+      },
+      setValue(val) {
+        if (this.equationUnit.activeIndex != undefined) {
+          if (
+            !this.equationUnit.replaceOrAppend ||
+            this.equationUnit.replaceOrAppend === 'replace'
+          ) {
+            this.equationUnit.equation.splice(
+              this.equationUnit.activeIndex,
+              1,
+              val
+            );
+          } else if (this.equationUnit.replaceOrAppend === 'append') {
+            this.equationUnit.equation.splice(
+              this.equationUnit.activeIndex + 1,
+              0,
+              val
+            );
+            // 追加后activeIndex后移一位
+            this.$set(
+              this.equationUnit,
+              'activeIndex',
+              this.equationUnit.activeIndex + 1
+            );
+          }
+        } else {
+          this.equationUnit.equation.push(val);
+        }
+      },
+
+      init(list) {
+        this.list = JSON.parse(list);
+        this.editShow = false;
+        if (this.list.length) {
+          this.$nextTick(() => {
+            this.list.forEach((item) => {
+              this.$refs['customTextRef' + item.id][0].init({
+                form: item.value,
+                valueObj: item.valueObj,
+                equation: item.equation,
+                units: item.units
+              });
+            });
+          });
+        }
+      },
+      editShowFn({ templateDivRef, domObj }) {
+        if (!this.edit) {
+          return;
+        }
+        this.templateDivRef = templateDivRef;
+        this.$set(this, 'domObj', domObj);
+        this.editShow = true;
+      },
+      editInputChange(type) {
+        if (type == 'equation') {
+          this.visible = false;
+          this.domObj.equation = JSON.parse(
+            JSON.stringify(this.equationUnit.equation)
+          );
+        }
+        this.$nextTick(() => {
+          this.$refs[this.templateDivRef][0].editInputChange(this.domObj);
+        });
+      },
+      del(id) {
+        this.list = this.list.filter((item) => item.id != id);
+      },
+      addHtml(type) {
+        this.list.push({
+          id: generateRandomString(6),
+          type: type,
+          value: '',
+          valueObj: {}
+        });
+      },
+      copy(data) {
+        console.log(data, 'asdasd');
+        let id = generateRandomString(6);
+        this.list.push({
+          id,
+          type: 'customTable',
+          ...data
+        });
+        this.$nextTick(() => {
+          this.$refs['customTextRef' + id][0].init({
+            form: null,
+            ...data
+          });
+        });
+      },
+      getValue() {
+        this.list.forEach((item, index) => {
+          let { form, valueObj, equation, units } =
+            this.$refs['customTextRef' + item.id][0].getValue();
+          this.$set(this.list[index], 'value', form);
+          this.$set(this.list[index], 'valueObj', valueObj);
+          this.$set(this.list[index], 'equation', equation);
+          this.$set(this.list[index], 'units', units);
+        });
+        return this.list;
+      }
+    }
+  };
+</script>
+
+<style scoped lang="scss">
+  .listItem {
+    padding: 5px;
+    padding-top: 15px;
+    border: solid 1px #f1f1f1;
+    display: flex;
+    width: 100%;
+    align-items: center;
+    margin-top: 3px;
+    position: relative;
+  }
+  .sort-handle {
+    font-size: 16px;
+    position: absolute;
+    left: 0;
+    top: 0;
+  }
+  .move {
+    cursor: move;
+  }
+  .delete {
+    cursor: pointer;
+    left: 20px;
+    color: #f56c6c;
+  }
+  .listItem:hover {
+    .sort-handle {
+      display: block !important;
+    }
+  }
+</style>

+ 36 - 14
src/views/rulesManagement/releaseRules/components/permitAdd.vue

@@ -137,7 +137,7 @@
             label="启用日期"
             prop="startDate"
             :rules="
-              businessType != 3
+              businessType != 3 && formData.reportWorkType != 6
                 ? [
                     {
                       required: true,
@@ -173,7 +173,7 @@
             label="停用日期"
             prop="stopDate"
             :rules="
-              businessType != 3
+              businessType != 3 && formData.reportWorkType != 6
                 ? [
                     {
                       required: true,
@@ -294,7 +294,7 @@
           </el-form-item>
         </el-col>
       </el-row>
-      <el-row v-if="businessType != 3">
+      <el-row v-if="businessType != 3 && formData.reportWorkType != 6">
         <el-col :span="24">
           <el-form-item label="周期" prop="frequencyValue" required>
             <rule-cycle
@@ -307,7 +307,11 @@
         </el-col>
       </el-row>
 
-      <header-title title="规则明细" v-if="businessType != 3"> </header-title>
+      <header-title
+        title="规则明细"
+        v-if="businessType != 3 && formData.reportWorkType != 6"
+      >
+      </header-title>
 
       <el-tabs
         v-if="formData.recordTemplateStyle == '4' && businessType != 3"
@@ -324,7 +328,7 @@
       </el-tabs>
 
       <ele-pro-table
-        v-if="businessType != 3"
+        v-if="businessType != 3 && formData.reportWorkType != 6"
         ref="table"
         :columns="bankColumns"
         :datasource="detialsDatasource"
@@ -693,12 +697,24 @@
         v-if="businessType == 3"
       >
       </header-title>
+      <header-title
+        title="作业票模板
+        "
+        v-if="businessType == 1 && formData.reportWorkType == 6"
+      >
+      </header-title>
       <experimentationProcess
         :edit="type != 'detail'"
         :readonly="type == 'detail'"
         v-if="businessType == 3"
         ref="experimentationProcess"
       ></experimentationProcess>
+      <experimentationProcessNew
+        :edit="type != 'detail'"
+        ref="experimentationProcess"
+        :readonly="type == 'detail'"
+        v-if="businessType == 1 && formData.reportWorkType == 6"
+      ></experimentationProcessNew>
     </el-form>
     <template v-slot:footer>
       <el-button
@@ -756,11 +772,11 @@
       @chooseProcess="processChooseProcess"
     ></processModal>
 
-    <vendorDialog
+    <!-- <vendorDialog
       ref="supplierSelectionRef"
       @success="confirmStaffSelection1"
       :isAll="true"
-    ></vendorDialog>
+    ></vendorDialog> -->
   </ele-modal>
 </template>
 
@@ -804,6 +820,7 @@
   import { getTreeByPid } from '@/api/classifyManage';
   import processModal from './processModal.vue';
   import experimentationProcess from './experimentationProcess.vue';
+  import experimentationProcessNew from './experimentationProcessNew.vue';
   import ProductModalCorrelation from './ProductModal.vue';
   import { recordrulesexecutemethodPage } from '@/api/recordrulesexecutemethod/index';
   import { businessTypeList } from '@/views/regulationManagement/components/util';
@@ -820,7 +837,8 @@
       processModal,
       ProductModalCorrelation,
       experimentationProcess,
-      bindSubstanceList
+      bindSubstanceList,
+      experimentationProcessNew
       // vendorDialog
     },
     props: {
@@ -861,7 +879,7 @@
             slot: 'paramValue',
             minWidth: 261
           },
-    {
+          {
             prop: 'iotPointName',
             slot: 'iotPointName',
             label: '物联点位',
@@ -910,7 +928,7 @@
             slot: 'substanceName',
             minWidth: 110
           },
-      
+
           {
             prop: 'substanceCode',
             label: '设备编码',
@@ -1447,7 +1465,7 @@
             this.$refs.cycleMultipleRef?.setRecordRulesCycleList(
               this.formData.recordRulesCycleList
             );
-            if (this.businessType == 3) {
+            if (this.businessType == 3|| this.formData.reportWorkType == 6) {
               this.$refs.experimentationProcess.init(data.tempJson.tempJson);
             }
           });
@@ -1548,7 +1566,11 @@
           if (!valid) {
             return false;
           }
-          if (this.formData.details.length == 0 && this.businessType != 3) {
+          if (
+            this.formData.details.length == 0 &&
+            this.businessType != 3 &&
+            this.formData.reportWorkType != 6
+          ) {
             return this.$message.warning('至少选择一条规则项');
           }
 
@@ -1591,7 +1613,7 @@
           if (!valid) {
             return false;
           }
-          if (this.businessType != 3) {
+          if (this.businessType != 3 && this.formData.reportWorkType != 6) {
             if (this.formData.details.length == 0) {
               return this.$message.warning('至少选择一条规则项');
             }
@@ -1805,7 +1827,7 @@
           body.stopDate,
           'yyyy-MM-dd HH:mm:ss'
         );
-        if (this.businessType == 3) {
+        if (this.businessType == 3 || this.formData.reportWorkType == 6) {
           let tempJson = this.$refs.experimentationProcess.getValue();
           if (tempJson) {
             body.tempJson = {

+ 901 - 0
src/views/rulesManagement/releaseRules/components/templateDiv/customTableNew.vue

@@ -0,0 +1,901 @@
+<template>
+  <div ref="tableContainer" style="margin-top: 10px; position: relative">
+    <!-- 合并选择框 -->
+    <div v-if="isSelecting" class="selection-box" :style="selectionStyle"></div>
+
+    <!-- 操作按钮 -->
+    <div v-if="edit">
+      <el-button type="primary" @click="addColumn(-1)">新增列</el-button>
+      <el-button type="primary" @click="addRow(-1)">新增行</el-button>
+      <el-button type="primary" @click="copy">复制表格</el-button>
+      <el-button type="primary" @click="printTable">预览表格</el-button>
+      <el-checkbox v-model="isMerge" style="margin-left: 10px">合并单元格</el-checkbox>
+    </div>
+
+    <!-- 表格主体 -->
+    <div class="table" style="margin-top: 10px">
+      <table
+        class="custom-table"
+        @mousedown="startSelecting"
+        @mousemove="updateSelection"
+        @mouseup="stopSelecting"
+      >
+        <tbody>
+          <tr v-for="(row, rowIndex) in tableData" :key="'row-' + rowIndex">
+            <template v-for="(cell, colIndex) in row">
+              <td
+                v-if="visibleMap[rowIndex + '-' + colIndex]"
+                :key="cell.id"
+                :rowspan="cell.rowspan"
+                :colspan="cell.colspan"
+                :style="getCellStyle(cell)"
+                class="tableTd"
+                :data-row="rowIndex"
+                :data-col="colIndex"
+                @mouseenter="onCellEnter(rowIndex, colIndex)"
+                @contextmenu.prevent="onRightClick($event, rowIndex, colIndex)"
+              >
+                <!-- 表头操作图标 -->
+                <i
+                  v-if="edit && rowIndex === 0"
+                  class="el-icon-delete delete"
+                  @click.stop="removeColumn(colIndex)"
+                ></i>
+                <i
+                  v-if="edit && rowIndex === 0"
+                  class="el-icon-circle-plus-outline add"
+                  @click.stop="addColumn(colIndex)"
+                ></i>
+                <i
+                  v-if="edit && rowIndex !== 0 && isLastVisibleCell(rowIndex, colIndex)"
+                  class="el-icon-delete deleteRow"
+                  @click.stop="removeRow(rowIndex)"
+                ></i>
+                <i
+                  v-if="edit && rowIndex !== 0 && colIndex !== row.length - 1"
+                  class="el-icon-circle-plus-outline addRow"
+                  @click.stop="addRow(rowIndex)"
+                ></i>
+
+                <!-- 复选框模式 -->
+                <template v-if="cell.mode === 'checkbox'">
+                  <div class="checkbox-group">
+                    <div
+                      v-for="cb in cell.checkboxes"
+                      :key="cb.id"
+                      class="checkbox-item"
+                    >
+                      <input type="checkbox" v-model="cb.checked" />
+                      <input
+                        v-if="edit"
+                        type="text"
+                        v-model="cb.label"
+                        class="checkbox-label-input"
+                        @input="onCheckboxLabelInput(rowIndex, colIndex, cb.id)"
+                      />
+                      <span v-else>{{ cb.label }}</span>
+                      <i
+                        v-if="edit"
+                        class="el-icon-delete"
+                        @click.stop="removeCheckbox(rowIndex, colIndex, cb.id)"
+                      ></i>
+                    </div>
+                    <div
+                      v-if="edit"
+                      class="add-checkbox"
+                      @click="addCheckbox(rowIndex, colIndex)"
+                    >
+                      <i class="el-icon-circle-plus-outline"></i> 新增选项
+                    </div>
+                  </div>
+                </template>
+
+                <!-- 文本模式 -->
+                <template v-else>
+                  <textarea
+                    v-model="cell.value"
+                    class="templateInput"
+                    :id="cell.id"
+                    :ref="cell.id + 'ref'"
+                    :readonly="cell.readonly === 2 || readonly"
+                    @mousedown="onCellMouseDown"
+                    @click="inputClick(cell, rowIndex === 0 ? 'columns' : null, $event)"
+                    @input="onInput($event)"
+                    autocomplete="off"
+                  ></textarea>
+                </template>
+              </td>
+            </template>
+          </tr>
+        </tbody>
+      </table>
+    </div>
+  </div>
+</template>
+
+<script>
+import { generateRandomString } from '@/utils/util';
+import VueDraggable from 'vuedraggable';
+
+export default {
+  components: { VueDraggable },
+  props: {
+    id: { type: String, default: '' },
+    edit: { type: Boolean, default: true },
+    readonly: { type: Boolean, default: false }
+  },
+  data() {
+    return {
+      form: null,
+      valueObj: {},
+      tableData: [],
+      units: {},
+      equation: {},
+      domId: '',
+      currentRowIndex: 0,
+      currentColumnIndex: 0,
+      // 合并相关
+      isMerge: false,
+      isSelecting: false,
+      startX: 0,
+      startY: 0,
+      endX: 0,
+      endY: 0,
+      startRow: -1,
+      startCol: -1,
+      endRow: -1,
+      endCol: -1,
+      // 防抖定时器
+      calcTimer: null
+    };
+  },
+  computed: {
+    visibleMap() {
+      const map = {};
+      const rows = this.tableData.length;
+      if (!rows) return map;
+      const cols = this.tableData[0]?.length || 0;
+
+      for (let r = 0; r < rows; r++) {
+        for (let c = 0; c < cols; c++) {
+          const key = `${r}-${c}`;
+          let hidden = false;
+
+          for (let rr = r - 1; rr >= 0; rr--) {
+            for (let cc = 0; cc < this.tableData[rr].length; cc++) {
+              const cell = this.tableData[rr][cc];
+              if (
+                cell.rowspan > 1 &&
+                rr + cell.rowspan > r &&
+                cc <= c &&
+                cc + cell.colspan > c
+              ) {
+                hidden = true;
+                break;
+              }
+            }
+            if (hidden) break;
+          }
+
+          if (!hidden) {
+            for (let cc = c - 1; cc >= 0; cc--) {
+              const cell = this.tableData[r][cc];
+              if (cell.colspan > 1 && cc + cell.colspan > c) {
+                hidden = true;
+                break;
+              }
+            }
+          }
+
+          map[key] = !hidden;
+        }
+      }
+      return map;
+    },
+
+    selectionStyle() {
+      const container = this.$refs.tableContainer;
+      if (!container) return {};
+      const rect = container.getBoundingClientRect();
+      return {
+        position: 'absolute',
+        zIndex: 100,
+        left: `${Math.min(this.startX, this.endX) - rect.left}px`,
+        top: `${Math.min(this.startY, this.endY) - rect.top}px`,
+        width: `${Math.abs(this.endX - this.startX)}px`,
+        height: `${Math.abs(this.endY - this.startY)}px`,
+        border: '2px dashed black',
+        pointerEvents: 'none'
+      };
+    }
+  },
+  methods: {
+    // ========== 工具 ==========
+    getCellStyle(cell) {
+      let width = parseFloat(cell.style?.width || cell.width || 100);
+      if (isNaN(width)) width = 100;
+      return { ...cell.style, width: width + 'px' };
+    },
+
+    getInput(width) {
+      const w = parseFloat(width);
+      const safeWidth = isNaN(w) ? 100 : w;
+      return {
+        id: generateRandomString(5),
+        value: '',
+        rowspan: 1,
+        colspan: 1,
+        width: safeWidth,
+        style: { width: safeWidth },
+        readonly: 1,
+        mode: 'text',
+        checkboxes: []
+      };
+    },
+
+    getIndex(id) {
+      for (let r = 0; r < this.tableData.length; r++) {
+        for (let c = 0; c < this.tableData[r].length; c++) {
+          if (this.tableData[r][c].id === id)
+            return { rowIndex: r, colIndex: c };
+        }
+      }
+      return { rowIndex: -1, colIndex: -1 };
+    },
+
+    isLastVisibleCell(rowIndex, colIndex) {
+      const row = this.tableData[rowIndex];
+      for (let c = row.length - 1; c >= 0; c--) {
+        if (this.visibleMap[`${rowIndex}-${c}`]) {
+          return c === colIndex;
+        }
+      }
+      return false;
+    },
+
+    // ========== 自动调整高度 ==========
+    autoResizeAll() {
+      this.$nextTick(() => {
+        this.$el.querySelectorAll('.templateInput').forEach((ta) => {
+          ta.style.height = 'auto';
+          ta.style.height = ta.scrollHeight + 'px';
+        });
+      });
+    },
+
+    // ========== 行列操作 ==========
+    addColumn(colIndex) {
+      if (colIndex === -1) {
+        if (this.tableData.length === 0) {
+          this.tableData = [[this.getInput()]];
+        } else {
+          this.tableData.forEach((row) => row.push(this.getInput()));
+        }
+        this.$nextTick(this.autoResizeAll);
+        return;
+      }
+
+      let insertAt = colIndex + 1;
+      this.tableData.forEach((row) => {
+        const cell = row[colIndex];
+        if (cell && cell.colspan > 1) {
+          insertAt = Math.max(insertAt, colIndex + cell.colspan);
+        }
+      });
+
+      this.tableData.forEach((row) => {
+        let covered = false;
+        for (let c = 0; c < insertAt && c < row.length; c++) {
+          const cell = row[c];
+          if (cell.colspan > 1 && c + cell.colspan > insertAt) {
+            cell.colspan += 1;
+            covered = true;
+            break;
+          }
+        }
+        if (!covered) {
+          const raw = row[colIndex]?.style?.width ?? row[colIndex]?.width ?? 100;
+          row.splice(insertAt, 0, this.getInput(raw));
+        }
+      });
+      this.$nextTick(this.autoResizeAll);
+    },
+
+    removeColumn(colIndex) {
+      for (let r = 0; r < this.tableData.length; r++) {
+        const row = this.tableData[r];
+        for (let rr = r - 1; rr >= 0; rr--) {
+          for (let cc = 0; cc < this.tableData[rr].length; cc++) {
+            const cell = this.tableData[rr][cc];
+            if (
+              cell.rowspan > 1 &&
+              rr + cell.rowspan > r &&
+              cell.colspan > 1 &&
+              cc <= colIndex &&
+              cc + cell.colspan > colIndex
+            ) {
+              cell.colspan -= 1;
+              break;
+            }
+          }
+        }
+        for (let c = colIndex - 1; c >= 0; c--) {
+          const leftCell = row[c];
+          if (leftCell && leftCell.colspan > 1 && c + leftCell.colspan > colIndex) {
+            leftCell.colspan -= 1;
+            break;
+          }
+        }
+        const cell = row[colIndex];
+        if (cell && cell.colspan > 1 && colIndex + 1 < row.length) {
+          row[colIndex + 1].colspan = cell.colspan - 1;
+          row[colIndex + 1].rowspan = cell.rowspan;
+          row[colIndex + 1].style = { ...cell.style };
+        }
+        row.splice(colIndex, 1);
+      }
+      this.$nextTick(this.autoResizeAll);
+    },
+
+    addRow(rowIndex) {
+      if (rowIndex === -1) {
+        if (this.tableData.length === 0) {
+          this.tableData = [[this.getInput()]];
+        } else {
+          this.tableData.push(this.tableData[0].map(() => this.getInput()));
+        }
+        this.$nextTick(this.autoResizeAll);
+        return;
+      }
+
+      let insertAt = rowIndex + 1;
+      for (let c = 0; c < this.tableData[rowIndex].length; c++) {
+        const cell = this.tableData[rowIndex][c];
+        if (cell.rowspan > 1) {
+          insertAt = Math.max(insertAt, rowIndex + cell.rowspan);
+        }
+      }
+
+      const newRow = [];
+      for (let c = 0; c < this.tableData[0].length; c++) {
+        let covered = false;
+        for (let r = 0; r < insertAt && r < this.tableData.length; r++) {
+          const cell = this.tableData[r][c];
+          if (cell.rowspan > 1 && r + cell.rowspan > insertAt) {
+            cell.rowspan += 1;
+            covered = true;
+            break;
+          }
+        }
+        if (!covered) {
+          const raw = this.tableData[0][c]?.style?.width ?? this.tableData[0][c]?.width ?? 100;
+          newRow.push(this.getInput(raw));
+        }
+      }
+      this.tableData.splice(insertAt, 0, newRow);
+      this.$nextTick(this.autoResizeAll);
+    },
+
+    removeRow(rowIndex) {
+      for (let c = 0; c < this.tableData[rowIndex].length; c++) {
+        const cell = this.tableData[rowIndex][c];
+        if (cell.rowspan > 1) {
+          const next = this.tableData[rowIndex + 1]?.[c];
+          if (next) {
+            next.rowspan = cell.rowspan - 1;
+            next.colspan = cell.colspan;
+            next.value = cell.value;
+            next.style = { ...cell.style };
+          }
+        }
+      }
+      this.tableData.splice(rowIndex, 1);
+      this.$nextTick(this.autoResizeAll);
+    },
+
+    // ========== 合并 ==========
+    startSelecting(event) {
+      if (!this.isMerge) return;
+      this.isSelecting = true;
+      const rect = this.$refs.tableContainer.getBoundingClientRect();
+      this.startX = event.clientX;
+      this.startY = event.clientY;
+      this.endX = this.startX;
+      this.endY = this.startY;
+
+      const td = event.target.closest('td');
+      if (td) {
+        this.startRow = Number(td.dataset.row);
+        this.startCol = Number(td.dataset.col);
+        this.endRow = this.startRow;
+        this.endCol = this.startCol;
+      }
+    },
+
+    updateSelection(event) {
+      if (this.isSelecting) {
+        this.endX = event.clientX;
+        this.endY = event.clientY;
+      }
+    },
+
+    stopSelecting() {
+      if (!this.isSelecting) return;
+      this.isSelecting = false;
+      this.calculateSelectedItems();
+      this.startRow = this.startCol = this.endRow = this.endCol = -1;
+    },
+
+    onCellEnter(rowIndex, colIndex) {
+      if (this.isSelecting) {
+        this.endRow = rowIndex;
+        this.endCol = colIndex;
+      }
+    },
+
+    calculateSelectedItems() {
+      if (
+        this.startRow < 0 ||
+        this.startCol < 0 ||
+        this.endRow < 0 ||
+        this.endCol < 0
+      )
+        return;
+      if (this.startRow === this.endRow && this.startCol === this.endCol)
+        return;
+
+      const minRow = Math.min(this.startRow, this.endRow);
+      const maxRow = Math.max(this.startRow, this.endRow);
+      const minCol = Math.min(this.startCol, this.endCol);
+      const maxCol = Math.max(this.startCol, this.endCol);
+
+      if (minRow === 0 && maxRow > 0) return;
+
+      for (let r = minRow; r <= maxRow; r++) {
+        for (let c = minCol; c <= maxCol; c++) {
+          const cell = this.tableData[r]?.[c];
+          if (!cell) return;
+          if ((cell.rowspan > 1 || cell.colspan > 1) && (r !== minRow || c !== minCol)) {
+            return;
+          }
+        }
+      }
+
+      const cell = this.tableData[minRow][minCol];
+      this.$set(cell, 'rowspan', maxRow - minRow + 1);
+      this.$set(cell, 'colspan', maxCol - minCol + 1);
+
+      this.$nextTick(() => {
+        requestAnimationFrame(() => {
+          const ref = this.$refs[cell.id + 'ref'];
+          const ta = Array.isArray(ref) ? ref[0] : ref;
+          const td = ta?.closest?.('td');
+          if (td) {
+            const realWidth = td.offsetWidth;
+            this.$set(cell, 'width', realWidth);
+            this.$set(cell.style, 'width', realWidth);
+          }
+        });
+      });
+
+      this.startRow = this.startCol = this.endRow = this.endCol = -1;
+    },
+
+    // ========== 右键菜单 ==========
+    onRightClick(event, rowIndex, colIndex) {
+      event.preventDefault();
+      this.currentRowIndex = rowIndex;
+      this.currentColumnIndex = colIndex;
+
+      const existingMenu = document.querySelector('.custom-context-menu');
+      if (existingMenu) existingMenu.remove();
+
+      const menu = document.createElement('div');
+      menu.className = 'custom-context-menu';
+      menu.style.cssText = `
+        position: fixed;
+        left: ${event.clientX}px;
+        top: ${event.clientY}px;
+        background: white;
+        border: 1px solid #ccc;
+        box-shadow: 2px 2px 8px rgba(0,0,0,0.2);
+        z-index: 10000;
+        padding: 5px 0;
+        min-width: 120px;
+      `;
+
+      const addItem = (text, onClick, isSeparator = false) => {
+        if (isSeparator) {
+          const hr = document.createElement('hr');
+          hr.style.margin = '5px 0';
+          menu.appendChild(hr);
+          return;
+        }
+        const item = document.createElement('div');
+        item.textContent = text;
+        item.style.cssText = 'padding: 5px 15px; cursor: pointer; white-space: nowrap;';
+        item.onmouseenter = () => (item.style.backgroundColor = '#f0f0f0');
+        item.onmouseleave = () => (item.style.backgroundColor = '');
+        item.onclick = () => {
+          if (onClick) onClick();
+          menu.remove();
+          document.removeEventListener('click', closeMenu);
+        };
+        menu.appendChild(item);
+      };
+
+      const closeMenu = (e) => {
+        if (!menu.contains(e.target)) {
+          menu.remove();
+          document.removeEventListener('click', closeMenu);
+        }
+      };
+
+      if (rowIndex === 0) {
+        addItem('插入列', () => this.addColumn(colIndex));
+        addItem('删除列', () => this.removeColumn(colIndex));
+      } else {
+        const cell = this.tableData[rowIndex][colIndex];
+        addItem('插入行', () => this.addRow(rowIndex));
+        addItem('删除行', () => this.removeRow(rowIndex));
+        addItem('', null, true);
+        if (cell.mode === 'text' || !cell.mode) {
+          addItem('转为复选框模式', () => this.enableCheckboxMode(rowIndex, colIndex));
+        } else {
+          addItem('转为文本模式', () => this.disableCheckboxMode(rowIndex, colIndex));
+          addItem('添加复选框', () => this.addCheckbox(rowIndex, colIndex));
+        }
+      }
+
+      document.body.appendChild(menu);
+      setTimeout(() => document.addEventListener('click', closeMenu), 0);
+    },
+
+    // ========== 复选框 ==========
+    enableCheckboxMode(rowIndex, colIndex) {
+      const cell = this.tableData[rowIndex][colIndex];
+      if (!cell) return;
+      const currentText = cell.value || '';
+      this.$set(cell, 'mode', 'checkbox');
+      this.$set(cell, 'checkboxes', [{ id: generateRandomString(6), label: currentText, checked: false }]);
+      cell.value = '';
+    },
+
+    disableCheckboxMode(rowIndex, colIndex) {
+      const cell = this.tableData[rowIndex][colIndex];
+      if (!cell) return;
+      const text = cell.checkboxes.map(cb => cb.label).filter(Boolean).join('; ');
+      this.$set(cell, 'mode', 'text');
+      this.$set(cell, 'value', text);
+      this.$set(cell, 'checkboxes', []);
+    },
+
+    addCheckbox(rowIndex, colIndex) {
+      const cell = this.tableData[rowIndex][colIndex];
+      if (!cell || cell.mode !== 'checkbox') return;
+      if (!cell.checkboxes) cell.checkboxes = [];
+      cell.checkboxes.push({ id: generateRandomString(6), label: '', checked: false });
+      this.$forceUpdate();
+    },
+
+    removeCheckbox(rowIndex, colIndex, cbId) {
+      const cell = this.tableData[rowIndex][colIndex];
+      if (!cell || cell.mode !== 'checkbox') return;
+      const idx = cell.checkboxes.findIndex(cb => cb.id === cbId);
+      if (idx !== -1) {
+        cell.checkboxes.splice(idx, 1);
+        if (cell.checkboxes.length === 0) {
+          this.disableCheckboxMode(rowIndex, colIndex);
+        } else {
+          this.$forceUpdate();
+        }
+      }
+    },
+
+    onCheckboxLabelInput() {},
+
+    // ========== 输入 ==========
+    onInput(event) {
+      const ta = event.target;
+      ta.style.height = 'auto';
+      ta.style.height = ta.scrollHeight + 'px';
+      this.debouncedCalculation();
+    },
+
+    debouncedCalculation() {
+      if (this.calcTimer) clearTimeout(this.calcTimer);
+      this.calcTimer = setTimeout(() => {
+        this.calculation();
+        this.calcTimer = null;
+      }, 300);
+    },
+
+    calculation() {
+      this.$emit('calculation');
+    },
+
+    onCellMouseDown(event) {
+      if (this.isMerge) event.preventDefault();
+    },
+
+    inputClick(item, type, event) {
+      if (!this.edit) return;
+      this.domId = item.id;
+      const ref = this.$refs[item.id + 'ref'];
+      const ta = Array.isArray(ref) ? ref[0] : ref;
+      const td = ta?.closest?.('td');
+      const realWidth = td ? td.offsetWidth : parseFloat(item.style?.width || item.width || 100);
+
+      this.$emit('editShow', {
+        templateDivRef: 'customTextRef' + this.id,
+        domObj: {
+          ...item,
+          width: realWidth,
+          isNoWidth: type === 'columns',
+          equation: this.equation[item.id],
+          units: this.units[item.id] || {}
+        }
+      });
+    },
+
+    // ========== 数据通信 ==========
+    equationValue({ domId, value }) {
+      const { rowIndex, colIndex } = this.getIndex(domId);
+      if (rowIndex >= 0 && colIndex >= 0) {
+        this.$set(this.tableData[rowIndex][colIndex], 'value', value);
+      }
+    },
+
+    getValue() {
+      return {
+        form: null,
+        equation: this.equation,
+        units: this.units,
+        valueObj: {
+          tableData: this.tableData
+        }
+      };
+    },
+
+    init({ form, valueObj, equation, units }) {
+      this.form = form;
+      this.tableData = valueObj.tableData || valueObj.columns || [];
+      this.equation = equation || {};
+      this.units = units || {};
+      this.autoResizeAll();
+    },
+
+    editInputChange(domObj) {
+      const data = JSON.parse(JSON.stringify(domObj));
+      if (data.equation) this.equation[data.id] = data.equation;
+      if (data.units) this.units[data.id] = data.units;
+
+      const w = Number(data.width);
+      if (!isNaN(w)) {
+        data.style = { ...(data.style || {}), width: w };
+        data.width = w;
+      } else if (data.style && data.style.width != null) {
+        data.width = Number(data.style.width);
+      }
+
+      const { rowIndex, colIndex } = this.getIndex(this.domId);
+      if (rowIndex >= 0 && colIndex >= 0) {
+        this.$set(this.tableData[rowIndex], colIndex, data);
+      }
+    },
+
+    copy() {
+      const tableData = JSON.parse(JSON.stringify(this.tableData));
+      tableData.forEach(row => row.forEach(cell => cell.id = generateRandomString(5)));
+      this.$emit('copy', {
+        value: null,
+        equation: { ...this.equation },
+        units: { ...this.units },
+        valueObj: { tableData }
+      });
+    },
+
+    // ========== 打印 ==========
+    printTable() {
+      const tableEl = this.$el.querySelector('.custom-table');
+      if (!tableEl) return;
+
+      const clone = tableEl.cloneNode(true);
+      clone.querySelectorAll('.delete, .add, .deleteRow, .addRow, .selection-box')
+        .forEach(el => el.remove());
+
+      clone.querySelectorAll('textarea').forEach(ta => {
+        const div = document.createElement('div');
+        div.textContent = ta.value;
+        div.style.cssText = 'text-align:center;word-break:break-all;padding:4px;white-space:pre-wrap;';
+        ta.parentNode.replaceChild(div, ta);
+      });
+
+      for (let r = 0; r < this.tableData.length; r++) {
+        for (let c = 0; c < this.tableData[r].length; c++) {
+          const cell = this.tableData[r][c];
+          if (cell.mode === 'checkbox' && Array.isArray(cell.checkboxes)) {
+            const td = clone.querySelector(`td[data-row="${r}"][data-col="${c}"]`);
+            if (td) {
+              const container = document.createElement('div');
+              container.style.cssText = 'display:flex; flex-wrap:wrap; gap:5px; justify-content:center;';
+              cell.checkboxes.forEach(cb => {
+                const item = document.createElement('div');
+                item.style.cssText = 'display:inline-flex; align-items:center; gap:5px; white-space:nowrap;';
+                const symbolSpan = document.createElement('span');
+                symbolSpan.textContent = cb.checked ? '☑' : '☐';
+                symbolSpan.style.fontSize = '14px';
+                const labelSpan = document.createElement('span');
+                labelSpan.textContent = cb.label || '';
+                item.appendChild(symbolSpan);
+                item.appendChild(labelSpan);
+                container.appendChild(item);
+              });
+              td.innerHTML = '';
+              td.appendChild(container);
+            }
+          }
+        }
+      }
+
+      const iframe = document.createElement('iframe');
+      iframe.style.cssText = 'position:absolute;width:0;height:0;border:0;';
+      document.body.appendChild(iframe);
+      const doc = iframe.contentWindow.document;
+      doc.write(`
+        <!DOCTYPE html>
+        <html>
+          <head>
+            <meta charset="utf-8">
+            <style>
+              body { margin: 0; padding: 0; font-family: Arial, sans-serif; }
+              table { width: 100%; border-collapse: collapse; table-layout: auto; }
+              td { border: 1px solid #000; vertical-align: middle; text-align: center; padding: 1px; font-size: 12px;padding:0px }
+            </style>
+          </head>
+          <body>${clone.outerHTML}</body>
+        </html>
+      `);
+      doc.close();
+      iframe.contentWindow.focus();
+      iframe.contentWindow.print();
+      setTimeout(() => document.body.removeChild(iframe), 1000);
+    }
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+.custom-table {
+  border-collapse: collapse;
+  width: auto;
+  td {
+    border: 1px solid #ddd;
+    padding: 0;
+    min-height: 20px;
+    position: relative;
+  }
+}
+:deep(.templateInput) {
+  width: 100%;
+  min-height: 20px;
+  height: auto;
+  border: none;
+  text-align: center;
+  background-color: #fff;
+  resize: none;
+  padding: 4px;
+  box-sizing: border-box;
+  overflow: hidden;
+  display: block;
+  &:focus {
+    border-color: #66afe9;
+    outline: none;
+  }
+}
+.deleteRow {
+  display: block !important;
+  position: absolute;
+  bottom: 0px;
+  right: -15px;
+  color: #f56c6c;
+  z-index: 10;
+}
+.delete,
+.add,
+.addRow {
+  display: none !important;
+}
+.tableTd:hover {
+  .delete {
+    display: block !important;
+    position: absolute;
+    right: 0;
+    top: 0;
+    color: #f56c6c;
+    z-index: 10;
+  }
+  .add {
+    display: block !important;
+    position: absolute;
+    right: 20px;
+    top: 0;
+    color: #409eff;
+    z-index: 10;
+  }
+  .addRow {
+    display: block !important;
+    position: absolute;
+    bottom: 0px;
+    right: 10px;
+    color: #409eff;
+    z-index: 10;
+  }
+}
+.checkbox-group {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 8px;
+  padding: 4px;
+  .checkbox-item {
+    display: inline-flex;
+    align-items: center;
+    gap: 4px;
+    background: #fff;
+    input[type='checkbox'] {
+      margin: 0;
+      flex-shrink: 0;
+    }
+    .checkbox-label-input {
+      border: 1px solid #dcdfe6;
+      border-radius: 2px;
+      padding: 2px 4px;
+      font-size: 12px;
+      min-width: 4em;
+      width: auto;
+      background: #fff;
+      &:focus {
+        outline: none;
+        border-color: #409eff;
+      }
+    }
+    .el-icon-delete {
+      color: #f56c6c;
+      cursor: pointer;
+      font-size: 14px;
+      flex-shrink: 0;
+      &:hover {
+        opacity: 0.8;
+      }
+    }
+  }
+  .add-checkbox {
+    display: inline-flex;
+    align-items: center;
+    gap: 4px;
+    color: #409eff;
+    cursor: pointer;
+    font-size: 12px;
+    white-space: nowrap;
+    &:hover {
+      text-decoration: underline;
+    }
+  }
+}
+@media print {
+  .add-checkbox,
+  .checkbox-item .el-icon-delete {
+    display: none !important;
+  }
+  .checkbox-label-input {
+    border: none !important;
+    background: transparent !important;
+    padding: 0 !important;
+    min-width: auto !important;
+    width: auto !important;
+  }
+  .checkbox-item {
+    break-inside: avoid;
+  }
+}
+</style>

+ 2 - 0
src/views/system/organization/components/org-user-edit.vue

@@ -2,6 +2,8 @@
 <template>
   <ele-modal
     width="60%"
+      append-to-body
+
     :visible="visible"
     :close-on-click-modal="false"
     custom-class="ele-dialog-form"

+ 1 - 0
src/views/workforceManagement/team/components/staffSelection.vue

@@ -324,6 +324,7 @@
     handleClose() {
       this.staffList = [];
       this.selectStafflist = [];
+      this.userName = '';
       this.dialogVisible = false;
     }
   }