From 0c9660562a03191d44fc779a889d3da0dc624b6d Mon Sep 17 00:00:00 2001
From: 董国庆 <364620639@qq.com>
Date: 星期五, 25 七月 2025 10:47:19 +0800
Subject: [PATCH] 修改弹窗ui和客户反馈

---
 laboratory/src/components/DynamicComponent/index.vue |  544 +++++++++++++++++++++++++++++++++++++++++-------------
 1 files changed, 412 insertions(+), 132 deletions(-)

diff --git a/laboratory/src/components/DynamicComponent/index.vue b/laboratory/src/components/DynamicComponent/index.vue
index 08b2099..218e9c9 100644
--- a/laboratory/src/components/DynamicComponent/index.vue
+++ b/laboratory/src/components/DynamicComponent/index.vue
@@ -4,152 +4,122 @@
       <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}`"
-            v-model="item.data.content" 
-            height="200px"
-            placeholder="请输入内容..." 
-          />
+          <AiEditor :ref="`editor_${item.id}`" :value="item.data.content" height="400px" :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'">
+                  <template v-if="Array.isArray(scope.row[header.name])">
+                    <el-image
+                      v-for="(img, i) in scope.row[header.name]"
+                      :key="i"
+                      :src="getFullUrl(img)"
+                      :preview-src-list="scope.row[header.name].map(getFullUrl)"
+                      class="table-image"
+                    />
+                  </template>
+                  <template v-else>
+                    <el-image
+                      v-if="scope.row[header.name]"
+                      :src="getFullUrl(scope.row[header.name])"
+                      :preview-src-list="[getFullUrl(scope.row[header.name])]"
+                      class="table-image"
+                    />
+                  </template>
+                </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"
+          <el-upload v-if="editable" :action="uploadUrl" :headers="uploadHeaders" :file-list="item.data.fileList"
             :on-change="(file, fileList) => handleFileChange(idx, fileList)"
-            list-type="text"
-          >
+            :on-success="(res, file, fileList) => handleFileSuccess(res, file, fileList, idx)"
+            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" >
+              <span style="color: #409EFF; cursor: pointer;" @click="downloadFileByUrl(file.url, file.name)">{{ file.name }}</span>
+            </div>
+          </div>
         </div>
         <!-- 图片上传 -->
         <div v-else-if="item.type == 'imageUpload'">
-          <el-upload
-            action="#"
-            :file-list="item.data.imageList"
-            :on-change="(file, fileList) => handleImageChange(idx, fileList)"
-            :on-success="
-              (res, file, fileList) =>
-                handleImageSuccess(res, file, fileList, idx)
-            "
-            list-type="picture-card"
-          >
-            <i class="el-icon-plus"></i>
-            <div class="upload-text">上传图片</div>
-          </el-upload>
-          <div class="uploaf-notice">支持.jpg .png格式</div>
+          <div class="image-upload-container">
+            <el-upload v-if="editable" 
+            :action="uploadUrl" 
+            :headers="uploadHeaders"
+             :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"
+              :before-upload="beforeImageUpload" 
+              list-type="picture-card"
+              :on-preview="(file) => handlePreview(file, idx)" class="image-uploader">
+              <i class="el-icon-plus"></i>
+              <div class="upload-text">上传图片</div>
+            </el-upload>
+            <div v-else class="image-preview">
+              <el-image v-for="image in item.data.imageList" :key="image.uid" :src="getFullUrl(image.url)"
+                :preview-src-list="item.data.imageList.map(img => getFullUrl(img.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"
-      @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>
+
+    <el-dialog :visible.sync="imagePreviewVisible" width="auto" top="10vh" :show-close="true" v-if="imagePreviewUrl">
+      <img :src="imagePreviewUrl" style="max-width:80vw;max-height:70vh;display:block;margin:auto;" />
+    </el-dialog>
   </div>
 </template>
 
@@ -159,6 +129,9 @@
 import Table from "../Table/index.vue";
 import addTableHeader from "./addTableHeader.vue";
 import addTableData from "./addTableData.vue";
+import apiConfig from '../../utils/baseurl'
+import { getFullUrl } from '@/utils/utils'
+import { downloadFileByUrl } from '@/utils/utils'
 
 export default {
   name: "DynamicComponent",
@@ -174,9 +147,30 @@
       type: String,
       default: "",
     },
+    participants: {
+      type: Array,
+      default: () => []
+    },
+    dataSource: {
+      type: Array,
+      default: () => []
+    },
+    editable: {
+      type: Boolean,
+      default: true
+    },
+    dialogCanEdit: {
+      type: Boolean,
+      default: true
+    }
   },
   data() {
     return {
+      apiConfig: apiConfig,
+      uploadUrl: apiConfig.imgUrl,
+      uploadHeaders: {
+        Authorization: sessionStorage.getItem('token') || ''
+      },
       showAddDialog: false,
       components: [],
       tableHeaderDialog: {
@@ -192,11 +186,112 @@
         headers: [],
         form: {},
       },
-      headerList: [], //编辑的表头列表
+      headerList: [],
+      defaultImageUrl: 'https://fuss10.elemecdn.com/e/5d/4a731a90594a4af544c0c25941171jpeg.jpeg', // 默认图片地址
+      imagePreviewVisible: false,
+      imagePreviewUrl: '',
     };
   },
+  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 };
+                console.log('component.data component.data',component.data)
+                break;
+              case 'imageUpload':
+                componentData = { imageList: component.data.map(item=>{
+                  return {
+                    ...item,
+                    url: getFullUrl(item.url),
+                  }
+                }) };
+                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: {
+    getFullUrl,
+    downloadFileByUrl,
+    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('请先选择实验调度');
+          this.showAddDialog = false;
+          return;
+        }
+      }
+
       this.showAddDialog = false;
       const id = Date.now() + Math.random();
       let data = {};
@@ -204,12 +299,97 @@
       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;
+        const prefix = apiConfig.showImgUrl;
+
+        switch (component.type) {
+          case 'richText':
+            const editorRef = this.$refs[`editor_${component.id}`];
+            const editor = Array.isArray(editorRef) ? editorRef[0] : editorRef;
+            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.map(file => {
+              console.log('fileUpload fileUpload fileUpload',file)
+              if (file.url && file.url.startsWith(prefix)) {
+                return { ...file, url: file.url.substring(prefix.length) };
+              }
+              return file;
+            });
+            break;
+          case 'imageUpload':
+            componentData = component.data.imageList.map(image => {
+              if (image.url && image.url.startsWith(prefix)) {
+                return { ...image, url: image.url.substring(prefix.length) };
+              }
+              return image;
+            });
+            break;
+        }
+
+        return {
+          type: component.type,
+          data: componentData
+        };
+      });
+
+      this.$emit('submit', data);
+    },
+    confirmAddRow(formData) {
+      // if (!this.editable) return;
+
+      const { idx, rowIndex, isEdit } = this.rowDialog;
+
+      // 处理formData中的数据,保证用户信息的完整性
+      const processedData = { ...formData };
+
+      // 调试输出
+      if (isEdit) {
+        // 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({
+          ...processedData,
+          updateTime: new Date().toLocaleString()
+        });
+      }
+
+      // 手动触发组件更新
+      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;
@@ -217,14 +397,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, []);
@@ -232,7 +410,6 @@
             this.$set(row, data.name, '');
           }
         } else {
-          // 如果行数据不是对象,转换为对象
           const newRow = {};
           this.components[idx].data.headers.forEach(header => {
             if (header.name === data.name) {
@@ -245,19 +422,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;
@@ -266,7 +442,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] = [];
@@ -281,12 +456,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: "取消",
@@ -295,20 +471,64 @@
         .then(() => {
           this.components[idx].data.rows.splice(rowIndex, 1);
           this.$message.success("删除成功");
+          this.emitUpdate();
         })
-        .catch(() => {});
+        .catch(() => { });
     },
     handleFileChange(idx, fileList) {
+      if (!this.editable) return;
+      // 只做 fileList 同步
       this.components[idx].data.fileList = fileList;
+      this.emitUpdate();
+    },
+    handleFileSuccess(res, file, fileList, idx) {
+      // 上传成功后设置真实 url
+      file.url = this.getFullUrl(res.msg);
+      this.components[idx].data.fileList = fileList;
+      this.emitUpdate();
     },
     handleImageChange(idx, fileList) {
+      if (!this.editable) return;
+      // 只做 imageList 同步
       this.components[idx].data.imageList = fileList;
+      this.emitUpdate();
     },
     handleImageSuccess(res, file, fileList, idx) {
-      // 假设后端返回的图片地址在 res.url
-      file.url = res.url;
+      // 上传成功后设置真实 url
+      file.url = this.getFullUrl(res.msg);
       this.components[idx].data.imageList = fileList;
+      this.emitUpdate();
     },
+    beforeImageUpload(file) {
+      const isJPG = file.type === 'image/jpeg';
+      const isPNG = file.type === 'image/png';
+      // const isLt2M = file.size / 1024 / 1024 < 2;
+
+      if (!isJPG && !isPNG) {
+        this.$message.error('上传图片只能是 JPG 或 PNG 格式!');
+        return false;
+      }
+      // if (!isLt2M) {
+      //   this.$message.error('上传图片大小不能超过 2MB!');
+      //   return false;
+      // }
+      this.imagePreviewVisible = true;
+      return true;
+    },
+    handlePreview(file, idx) {
+      // 使用el-image的preview-src-list实现预览
+      // 这里直接用Element的图片预览能力,实际上el-upload会自动处理
+      // 但如果你想自定义弹窗,可以用如下代码:
+      this.imagePreviewUrl = this.getFullUrl(file.url);
+      this.imagePreviewVisible = true;
+    },
+    emitUpdate() {
+      // 先创建新对象,这有助于触发更新
+      const updatedComponents = JSON.parse(JSON.stringify(this.components));
+      this.$emit('update:dataSource', updatedComponents);
+    },
+  },
+  computed: {
   },
 };
 </script>
@@ -319,9 +539,11 @@
   padding: 20px;
   margin-top: 37px;
 }
-.has-title{
+
+.has-title {
   margin-top: 0px !important;
 }
+
 .add-group {
   display: flex;
   align-items: center;
@@ -339,18 +561,47 @@
     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 {
+  display: flex;
+
+  ::v-deep .el-upload-list__item {
+    width: 104px !important;
+    height: 104px !important;
+  }
+}
+
+.image-upload-container {
+  display: flex;
+  flex-wrap: wrap;
+  align-items: flex-start;
+  gap: 10px;
+}
+
+::v-deep .image-uploader {
+  .el-upload--picture-card {
+    margin: 0;
+  }
+
+  .el-upload-list--picture-card .el-upload-list__item {
+    margin: 0 10px 10px 0;
+  }
+}
+
 ::v-deep .el-upload {
   width: 104px;
   height: 104px;
@@ -358,6 +609,7 @@
   align-items: center;
   justify-content: center;
   flex-direction: column;
+
   .upload-text {
     font-weight: 400;
     font-size: 14px;
@@ -366,6 +618,7 @@
     margin-top: 13px;
   }
 }
+
 .uploaf-notice {
   font-weight: 400;
   font-size: 14px;
@@ -373,17 +626,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>

--
Gitblit v1.7.1