hejianhao
7 天以前 86c6153f294e61c0ee2268b2651c63313abb56ee
laboratory/src/components/DynamicComponent/index.vue
@@ -4,155 +4,96 @@
      <div class="add-group">
        <div v-if="title">*</div>
        <span v-if="title">{{ title }}</span>
        <el-button
          @click="showAddDialog = true"
          class="el-icon-plus"
          type="primary"
        >
          添加组件</el-button
        >
        <el-button v-if="editable" @click="showAddDialog = true" class="el-icon-plus" type="primary">
          添加组件</el-button>
      </div>
      <!-- 选择组件弹窗 -->
      <AddComponentDialog
        :visible="showAddDialog"
        @confirm="addComponent"
        @close="showAddDialog = false"
      />
      <AddComponentDialog v-if="editable" :visible="showAddDialog" @confirm="addComponent"
        @close="showAddDialog = false" />
      <!-- 动态渲染组件 -->
      <div
        v-for="(item, idx) in components"
        :key="item.id"
        class="dynamic-component"
      >
      <div v-for="(item, idx) in components" :key="item.id" class="dynamic-component">
        <!-- 富文本 -->
        <div v-if="item.type == 'richText'">
          <AiEditor
            :ref="`editor_${item.id}`"
           :value="item.data.content"
            height="200px"
            placeholder="请输入内容..."
          />
          <AiEditor :ref="`editor_${item.id}`" :value="item.data.content" height="200px" :readOnly="!editable" placeholder="请输入内容..."
            :disabled="!editable" />
        </div>
        <!-- 自定义表格 -->
        <div v-else-if="item.type == 'customTable'" style="flex: 1">
          <div class="table-actions">
            <el-button size="mini" @click="showTableHeaderDialog(idx)"
              >添加表头</el-button
            >
            <el-button
              size="mini"
              type="primary"
              @click="showAddRowDialog(idx, item.data.headers)"
              >添加数据</el-button
            >
          <div v-if="editable" class="table-actions">
            <el-button size="mini"  @click="showTableHeaderDialog(idx)">添加表头</el-button>
            <el-button size="mini" type="primary" @click="showAddRowDialog(idx, item.data.headers)">添加数据</el-button>
          </div>
          <Table
            :data="item.data.rows"
            :total="null"
            :height="null"
            class="groupTable"
          >
            <!-- <el-table-column
              type="index"
              label="序号"
              width="80"
            ></el-table-column> -->
            <el-table-column
              v-for="(header, hidx) in item.data.headers"
              :key="hidx"
              :label="header.name"
              :prop="header.name"
            />
            <el-table-column
              label="更新时间"
              prop="updateTime"
              min-width="180"
            ></el-table-column>
            <el-table-column label="操作" min-width="200">
          <Table :data="item.data.rows" :total="null" :height="null" class="groupTable" :key="item.id + '_' + JSON.stringify(item.data.rows).length">
            <el-table-column v-for="(header, hidx) in item.data.headers" :key="hidx" :label="header.name"
              :prop="header.name">
              <template slot-scope="scope">
                <el-button type="text" @click="handleEditRow(idx, scope.$index)"
                  >编辑</el-button
                >
                <el-button
                  type="text"
                  @click="handleDeleteRow(idx, scope.$index)"
                  >删除</el-button
                >
                <!-- 用户类型显示 -->
                <template v-if="header.type === 'user'">
                  {{ getUserDisplayText(header.name, scope.row) }}
                </template>
                <!-- 图片类型显示 -->
                <template v-else-if="header.type === 'image'">
                  <img v-if="scope.row[header.name]"
                       :src="scope.row[header.name]"
                       alt="头像"
                       class="table-image" />
                </template>
                <!-- 其他类型 -->
                <template v-else>
                  {{ scope.row[header.name] }}
                </template>
              </template>
            </el-table-column>
            <el-table-column label="更新时间" prop="updateTime" min-width="180"></el-table-column>
            <el-table-column  label="操作" min-width="200" v-if="dialogCanEdit">
              <template slot-scope="scope">
                <el-button type="text" @click="handleEditRow(idx, scope.$index)" >编辑</el-button>
                <el-button type="text" v-if="editable" @click="handleDeleteRow(idx, scope.$index)">删除</el-button>
              </template>
            </el-table-column>
          </Table>
        </div>
        <!-- 文件上传 -->
        <div v-else-if="item.type == 'fileUpload'">
          <el-upload
            action="#"
            :file-list="item.data.fileList"
            :on-change="(file, fileList) => handleFileChange(idx, fileList)"
            list-type="text"
          >
          <el-upload v-if="editable" action="#" :file-list="item.data.fileList"
            :on-change="(file, fileList) => handleFileChange(idx, fileList)" list-type="text">
            <el-button size="small" icon="el-icon-upload2">点击上传</el-button>
          </el-upload>
          <div v-else>
            <div v-for="file in item.data.fileList" :key="file.uid" class="file-list-item">
              {{ file.name }}
            </div>
          </div>
        </div>
        <!-- 图片上传 -->
        <div v-else-if="item.type == 'imageUpload'">
          <div class="image-upload-container">
            <el-upload
              action="#"
              :file-list="item.data.imageList"
            <el-upload v-if="editable" action="#" :file-list="item.data.imageList"
              :on-change="(file, fileList) => handleImageChange(idx, fileList)"
              :on-success="(res, file, fileList) => handleImageSuccess(res, file, fileList, idx)"
              :auto-upload="true"
              :http-request="() => {}"
              :before-upload="beforeImageUpload"
              list-type="picture-card"
              class="image-uploader"
            >
              :on-success="(res, file, fileList) => handleImageSuccess(res, file, fileList, idx)" :auto-upload="true"
              :http-request="() => { }" :before-upload="beforeImageUpload" list-type="picture-card"
              class="image-uploader">
              <i class="el-icon-plus"></i>
              <div class="upload-text">上传图片</div>
            </el-upload>
            <div v-else class="image-preview">
              <img v-for="image in item.data.imageList" :key="image.uid" :src="image.url" class="preview-image" />
            </div>
            <div class="uploaf-notice">支持.jpg .png格式</div>
          </div>
        </div>
        <img
          src="@/assets/public/delete.png"
          @click="removeComponent(idx)"
          alt="删除"
          class="delete-icon"
        />
        <img v-if="editable" src="@/assets/public/delete.png" @click="removeComponent(idx)" alt="删除"
          class="delete-icon" />
      </div>
    </div>
    <!-- 添加表头弹窗 -->
    <el-dialog
      :visible.sync="tableHeaderDialog.visible"
      title="添加表头"
      width="300px"
    >
      <el-input
        v-model="tableHeaderDialog.header"
        placeholder="请输入表头"
      ></el-input>
      <span slot="footer" class="dialog-footer">
        <el-button @click="tableHeaderDialog.visible = false">取消</el-button>
        <el-button type="primary" @click="confirmAddHeader">确定</el-button>
      </span>
    </el-dialog>
    <addTableHeader
      :visible.sync="tableHeaderDialog.visible"
      :participants="participants"
      @confirm="confirmAddHeader"
    ></addTableHeader>
    <addTableData
      :visible.sync="rowDialog.visible"
      :headerList="rowDialog.headers"
      :editData="rowDialog.form"
      :isEdit="rowDialog.isEdit"
      @success="confirmAddRow"
    >
    <addTableHeader  :visible.sync="tableHeaderDialog.visible" :participants="participants"
      @confirm="confirmAddHeader"></addTableHeader>
    <addTableData  :visible.sync="rowDialog.visible" :headerList="rowDialog.headers"
      :editData="rowDialog.form" :isEdit="rowDialog.isEdit" @success="confirmAddRow">
    </addTableData>
  </div>
</template>
@@ -181,6 +122,18 @@
    participants: {
      type: Array,
      default: () => []
    },
    dataSource: {
      type: Array,
      default: () => []
    },
    editable: {
      type: Boolean,
      default: true
    },
    dialogCanEdit: {
      type: Boolean,
      default: true
    }
  },
  data() {
@@ -200,12 +153,94 @@
        headers: [],
        form: {},
      },
      headerList: [], //编辑的表头列表
      headerList: [],
      defaultImageUrl: 'https://fuss10.elemecdn.com/e/5d/4a731a90594a4af544c0c25941171jpeg.jpeg', // 默认图片地址
    };
  },
  watch: {
    dataSource: {
      handler(newVal) {
        if (newVal) {
          newVal = newVal.map(component => {
            let componentData = null;
            switch (component.type) {
              case 'richText':
                componentData = { content: component.data }
                break;
              case 'customTable':
                componentData = {
                  headers: component.data.headers,
                  rows: component.data.rows
                };
                break;
              case 'fileUpload':
                componentData = { fileList: component.data };
                break;
              case 'imageUpload':
                componentData = { imageList: component.data };
                break;
            }
            return {
              type: component.type,
              id: component.id || Math.random().toString(36).substr(2, 9),
              data: componentData
            }
          })
        }
        this.components = newVal ? [...newVal] : [];
      },
      immediate: true,
      deep: true
    }
  },
  methods: {
    getUserDisplayText(fieldName, rowData) {
      // 检查是否有对应的userInfo数据
      const userInfoKey = `${fieldName}_userInfo`;
      // 如果没有rowData或fieldName,直接返回空字符串
      if (!rowData || !fieldName) {
        return '';
      }
      // 情况1: 有_userInfo数据
      if (rowData[userInfoKey] && Array.isArray(rowData[userInfoKey])) {
        return rowData[userInfoKey].map(user => user.label || '').join(', ');
      }
      // 情况2: 只有用户ID数组
      if (Array.isArray(rowData[fieldName])) {
        // 使用participants查找用户信息
        return rowData[fieldName].map(userId => {
          const user = this.participants.find(p => p.userId === userId);
          return user ? (user.nickName || user.userName || userId) : userId;
        }).join(', ');
      }
      // 情况3: 已经是字符串(可能是之前格式化过的)
      if (typeof rowData[fieldName] === 'string') {
        return rowData[fieldName];
      }
      // 默认返回空字符串
      return '';
    },
    formatUserNames(userArray) {
      if (!userArray || !Array.isArray(userArray) || userArray.length === 0) {
        return '';
      }
      // 查找参与者列表中的用户,获取昵称并用逗号拼接
      const userNames = userArray.map(userId => {
        const user = this.participants.find(p => p.userId === userId);
        return user ? user.nickName : userId;
      });
      return userNames.join(', ');
    },
    addComponent(type) {
      // 如果是添加自定义表格,需要检查是否已选择实验调度
      if (!this.editable) return;
      if (type === "customTable") {
        if (!this.participants || this.participants.length === 0) {
          this.$message.warning('请先选择实验调度');
@@ -213,7 +248,7 @@
          return;
        }
      }
      this.showAddDialog = false;
      const id = Date.now() + Math.random();
      let data = {};
@@ -221,69 +256,88 @@
      if (type === "customTable") data = { headers: [], rows: [] };
      if (type === "fileUpload") data = { fileList: [] };
      if (type === "imageUpload") data = { imageList: [] };
      console.log(type, "111111111111111", this.components);
      this.components.push({ id, type, data });
      this.emitUpdate();
    },
    submit() {
      // 获取所有组件的数据
      const data = this.components.map(component => {
        let componentData = null;
        switch (component.type) {
          case 'richText':
            const editorRef = this.$refs[`editor_${component.id}`];
            const editor = Array.isArray(editorRef) ? editorRef[0] : editorRef;
            console.log('editor ref:', editor);
            const content = editor ? editor.getContent() : '';
            componentData = content && content !== '<p></p>' ? content : '';
            break;
          case 'customTable':
            // 获取表格数据
            componentData = {
              headers: component.data.headers,
              rows: component.data.rows
            };
            break;
          case 'fileUpload':
            // 获取文件列表
            componentData = component.data.fileList;
            break;
          case 'imageUpload':
            // 获取图片列表
            componentData = component.data.imageList;
            break;
        }
        return {
          type: component.type,
          data: componentData
        };
      });
      // 触发 submit 事件,将数据传递给父组件
      this.$emit('submit', data);
    },
    confirmAddRow(formData) {
      // if (!this.editable) return;
      const { idx, rowIndex, isEdit } = this.rowDialog;
      // 处理formData中的数据,保证用户信息的完整性
      const processedData = { ...formData };
      // 调试输出
      console.log('添加/编辑行数据:', processedData,'isEdit',isEdit);
      if (isEdit) {
        // 编辑模式
        this.components[idx].data.rows[rowIndex] = {
          ...formData,
          updateTime: new Date().toLocaleString()
        };
        // Vue无法检测到对象或数组深层属性的变化,使用Vue.set来确保响应式
        this.$set(
          this.components[idx].data.rows,
          rowIndex,
          {
            ...processedData,
            updateTime: new Date().toLocaleString()
          }
        );
      } else {
        // 新增模式
        // 使用数组方法push会被Vue检测到
        this.components[idx].data.rows.push({
          ...formData,
          ...processedData,
          updateTime: new Date().toLocaleString()
        });
      }
      console.log('this.components',this.components);
      // 手动触发组件更新
      this.$forceUpdate();
      // 延迟发送事件,确保数据已更新
      this.$nextTick(() => {
        this.emitUpdate();
      });
      this.rowDialog.visible = false;
      this.rowDialog.form = {};
    },
    removeComponent(idx) {
      if (!this.editable) return;
      this.components.splice(idx, 1);
      this.emitUpdate();
    },
    showTableHeaderDialog(idx) {
      this.tableHeaderDialog.visible = true;
@@ -291,14 +345,12 @@
      this.tableHeaderDialog.header = "";
    },
    confirmAddHeader(data) {
      console.log("data", data);
      if (!this.editable) return;
      const { idx } = this.tableHeaderDialog;
      // 添加新表头
      this.components[idx].data.headers.push({ ...data });
      // 为已有行数据添加新表头对应的默认值
      this.components[idx].data.rows.forEach(row => {
        // 如果行数据是对象,直接添加新属性
        if (typeof row === 'object' && row !== null) {
          if (data.type === 'user') {
            this.$set(row, data.name, []);
@@ -306,7 +358,6 @@
            this.$set(row, data.name, '');
          }
        } else {
          // 如果行数据不是对象,转换为对象
          const newRow = {};
          this.components[idx].data.headers.forEach(header => {
            if (header.name === data.name) {
@@ -319,19 +370,18 @@
              newRow[header.name] = row[header.name] || '';
            }
          });
          // 替换原行数据
          const rowIndex = this.components[idx].data.rows.indexOf(row);
          this.components[idx].data.rows.splice(rowIndex, 1, newRow);
        }
      });
      // 关闭弹窗并重置数据
      this.tableHeaderDialog.visible = false;
      this.tableHeaderDialog = {
        visible: false,
        idx: null,
        header: "",
      };
      this.emitUpdate();
    },
    showAddRowDialog(idx, headerList) {
      this.headerList = headerList;
@@ -340,7 +390,6 @@
      this.rowDialog.isEdit = false;
      this.rowDialog.headers = this.components[idx].data.headers;
      this.rowDialog.form = {};
      // 初始化表单数据
      this.rowDialog.headers.forEach((header) => {
        if (header.type === "user") {
          this.rowDialog.form[header.name] = [];
@@ -355,12 +404,13 @@
      this.rowDialog.rowIndex = rowIndex;
      this.rowDialog.isEdit = true;
      this.rowDialog.headers = this.components[idx].data.headers;
      // 深拷贝行数据,避免直接修改原数据
      this.rowDialog.form = JSON.parse(
        JSON.stringify(this.components[idx].data.rows[rowIndex])
      );
    },
    handleDeleteRow(idx, rowIndex) {
      if (!this.editable) return;
      this.$confirm("确认删除该行数据吗?", "提示", {
        confirmButtonText: "确定",
        cancelButtonText: "取消",
@@ -369,11 +419,13 @@
        .then(() => {
          this.components[idx].data.rows.splice(rowIndex, 1);
          this.$message.success("删除成功");
          this.emitUpdate();
        })
        .catch(() => {});
        .catch(() => { });
    },
    handleFileChange(idx, fileList) {
      // 为每个文件添加默认的URL和名称
      if (!this.editable) return;
      fileList = fileList.map(file => {
        if (!file.url) {
          file.url = 'https://picsum.photos/200/200';
@@ -384,9 +436,11 @@
        return file;
      });
      this.components[idx].data.fileList = fileList;
      this.emitUpdate();
    },
    handleImageChange(idx, fileList) {
      // 为每个文件添加默认的URL
      if (!this.editable) return;
      fileList = fileList.map(file => {
        if (!file.url) {
          file.url = 'https://picsum.photos/200/200';
@@ -394,9 +448,9 @@
        return file;
      });
      this.components[idx].data.imageList = fileList;
      this.emitUpdate();
    },
    handleImageSuccess(res, file, fileList, idx) {
      // 使用默认图片URL
      file.url = 'https://picsum.photos/200/200';
      this.components[idx].data.imageList = fileList;
    },
@@ -415,6 +469,11 @@
      }
      return true;
    },
    emitUpdate() {
      // 先创建新对象,这有助于触发更新
      const updatedComponents = JSON.parse(JSON.stringify(this.components));
      this.$emit('update:dataSource', updatedComponents);
    },
  },
};
</script>
@@ -425,9 +484,11 @@
  padding: 20px;
  margin-top: 37px;
}
.has-title{
.has-title {
  margin-top: 0px !important;
}
.add-group {
  display: flex;
  align-items: center;
@@ -445,25 +506,30 @@
    margin: 0 32px 0 8px;
  }
}
.dynamic-component {
  background: #ffffff;
  padding: 15px 20px;
  display: flex;
  justify-content: space-between;
  margin-bottom: 20px;
  .delete-icon {
    width: 16px;
    height: 16px;
    cursor: pointer;
  }
}
.image-uploader{
.image-uploader {
  display: flex;
 ::v-deep .el-upload-list__item{
width: 104px !important;
height: 104px !important;
  ::v-deep .el-upload-list__item {
    width: 104px !important;
    height: 104px !important;
  }
}
.image-upload-container {
  display: flex;
  flex-wrap: wrap;
@@ -475,6 +541,7 @@
  .el-upload--picture-card {
    margin: 0;
  }
  .el-upload-list--picture-card .el-upload-list__item {
    margin: 0 10px 10px 0;
  }
@@ -487,6 +554,7 @@
  align-items: center;
  justify-content: center;
  flex-direction: column;
  .upload-text {
    font-weight: 400;
    font-size: 14px;
@@ -495,6 +563,7 @@
    margin-top: 13px;
  }
}
.uploaf-notice {
  font-weight: 400;
  font-size: 14px;
@@ -502,17 +571,44 @@
  line-height: 22px;
  margin-top: 8px;
}
.table-actions {
  margin-bottom: 10px;
  display: flex;
  gap: 10px;
}
.groupTable {
  width: 100%;
  margin-top: 10px;
  ::v-deep .el-input__inner {
    width: unset !important;
  }
}
.file-list-item {
  padding: 8px 0;
  border-bottom: 1px solid #eee;
}
.image-preview {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
}
.preview-image {
  width: 104px;
  height: 104px;
  object-fit: cover;
  border-radius: 8px;
}
.table-image {
  width: 50px;
  height: 50px;
  object-fit: cover;
  border-radius: 4px;
}
</style>