董国庆
2025-09-12 c3ccabfc59431aea657379c72c2e09c8e1b27d18
图片回显
4个文件已修改
354 ■■■■■ 已修改文件
laboratory/src/components/AiEditor/index.vue 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
laboratory/src/components/DynamicComponent/index.vue 339 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
laboratory/src/utils/utils.js 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
laboratory/src/views/dataManagement/sampleRecordList/components/sampleDialog.vue 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
laboratory/src/components/AiEditor/index.vue
@@ -7,7 +7,7 @@
<script>
import { AiEditor } from "aieditor";
import 'aieditor/dist/style.css'
// import { customUploadRequest, getFullUrl } from '@/utils/utils'
import { getAllocateIp } from '@/utils/utils'
import apiConfig from '@/utils/baseurl'
export default {
@@ -123,7 +123,7 @@
                  let data = {
                    "errorCode": 0,
                    "data": {
                      "src": apiConfig.showImgUrl + json.msg,
                      "src": getAllocateIp() + json.msg,
                      "alt": "图片 alt"
                    }
                  }
@@ -153,7 +153,7 @@
                  let data = {
                    "errorCode": 0,
                    "data": {
                      "src": apiConfig.showImgUrl + json.msg,
                      "src": getAllocateIp() + json.msg,
                      "poster": "http://your-domain.com/poster.jpg"
                    }
                  }
@@ -190,7 +190,7 @@
                  let data = {
                    "errorCode": 0,
                    "data": {
                      "href": apiConfig.showImgUrl + json.msg,
                      "href": getAllocateIp() + json.msg,
                      "fileName": file.name
                    }
                  }
laboratory/src/components/DynamicComponent/index.vue
@@ -4,31 +4,67 @@
      <div class="add-group">
        <div v-if="title">*</div>
        <span v-if="title">{{ title }}</span>
        <el-button v-if="editable" @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 v-if="editable" :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="400px" :readOnly="!editable"
            placeholder="请输入内容..." :disabled="!editable" />
          <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 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>
            <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"
            :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">
          <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">
                <!-- 用户类型显示 -->
                <template v-if="header.type === 'user'">
@@ -60,65 +96,128 @@
                </template>
              </template>
            </el-table-column>
            <el-table-column label="更新时间" prop="updateTime" min-width="180"></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>
                <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 v-if="editable" :action="uploadUrl" :headers="uploadHeaders" :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)"
            :on-success="(res, file, fileList) => handleFileSuccess(res, file, fileList, idx)"
            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
              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'">
          <div class="image-upload-container">
            <el-upload v-if="editable"
            :action="uploadUrl"
            :headers="uploadHeaders"
             :file-list="item.data.imageList"
            <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"
              :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">
              :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" />
              <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 v-if="editable" 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>
    <addTableHeader :visible.sync="tableHeaderDialog.visible" :participants="participants" @confirm="confirmAddHeader">
    <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
      :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
      :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>
@@ -129,9 +228,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'
import apiConfig from "../../utils/baseurl";
import { getFullUrl } from "@/utils/utils";
import { downloadFileByUrl,getAllocateIp } from "@/utils/utils";
export default {
  name: "DynamicComponent",
@@ -149,27 +248,27 @@
    },
    participants: {
      type: Array,
      default: () => []
      default: () => [],
    },
    dataSource: {
      type: Array,
      default: () => []
      default: () => [],
    },
    editable: {
      type: Boolean,
      default: true
      default: true,
    },
    dialogCanEdit: {
      type: Boolean,
      default: true
    }
      default: true,
    },
  },
  data() {
    return {
      apiConfig: apiConfig,
      uploadUrl: apiConfig.imgUrl,
      uploadHeaders: {
        Authorization: sessionStorage.getItem('token') || ''
        Authorization: sessionStorage.getItem("token") || "",
      },
      showAddDialog: false,
      components: [],
@@ -187,53 +286,56 @@
        form: {},
      },
      headerList: [],
      defaultImageUrl: 'https://fuss10.elemecdn.com/e/5d/4a731a90594a4af544c0c25941171jpeg.jpeg', // 默认图片地址
      defaultImageUrl:
        "https://fuss10.elemecdn.com/e/5d/4a731a90594a4af544c0c25941171jpeg.jpeg", // 默认图片地址
      imagePreviewVisible: false,
      imagePreviewUrl: '',
      imagePreviewUrl: "",
    };
  },
  watch: {
    dataSource: {
      handler(newVal) {
        if (newVal) {
          newVal = newVal.map(component => {
          newVal = newVal.map((component) => {
            let componentData = null;
            switch (component.type) {
              case 'richText':
                componentData = { content: component.data }
              case "richText":
                componentData = { content: component.data };
                break;
              case 'customTable':
              case "customTable":
                componentData = {
                  headers: component.data.headers,
                  rows: component.data.rows
                  rows: component.data.rows,
                };
                break;
              case 'fileUpload':
              case "fileUpload":
                componentData = { fileList: component.data };
                console.log('component.data component.data',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),
                  }
                }) };
              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
            }
          })
              data: componentData,
            };
          });
        }
        this.components = newVal ? [...newVal] : [];
      },
      immediate: true,
      deep: true
    }
      deep: true,
    },
  },
  methods: {
    getFullUrl,
@@ -244,49 +346,51 @@
      // 如果没有rowData或fieldName,直接返回空字符串
      if (!rowData || !fieldName) {
        return '';
        return "";
      }
      // 情况1: 有_userInfo数据
      if (rowData[userInfoKey] && Array.isArray(rowData[userInfoKey])) {
        return rowData[userInfoKey].map(user => user.label || '').join(', ');
        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(', ');
        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') {
      if (typeof rowData[fieldName] === "string") {
        return rowData[fieldName];
      }
      // 默认返回空字符串
      return '';
      return "";
    },
    formatUserNames(userArray) {
      if (!userArray || !Array.isArray(userArray) || userArray.length === 0) {
        return '';
        return "";
      }
      // 查找参与者列表中的用户,获取昵称并用逗号拼接
      const userNames = userArray.map(userId => {
        const user = this.participants.find(p => p.userId === userId);
      const userNames = userArray.map((userId) => {
        const user = this.participants.find((p) => p.userId === userId);
        return user ? user.nickName : userId;
      });
      return userNames.join(', ');
      return userNames.join(", ");
    },
    addComponent(type) {
      if (!this.editable) return;
      if (type === "customTable") {
        if (!this.participants || this.participants.length === 0) {
          this.$message.warning('请先选择实验调度');
          this.$message.warning("请先选择实验调度");
          this.showAddDialog = false;
          return;
        }
@@ -304,34 +408,34 @@
      this.emitUpdate();
    },
    submit() {
      const data = this.components.map(component => {
      const data = this.components.map((component) => {
        let componentData = null;
        const prefix = apiConfig.showImgUrl;
        // const prefix = apiConfig.showImgUrl;
        const prefix = getAllocateIp();
        switch (component.type) {
          case 'richText':
          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 : '';
            const content = editor ? editor.getContent() : "";
            componentData = content && content !== "<p></p>" ? content : "";
            break;
          case 'customTable':
          case "customTable":
            componentData = {
              headers: component.data.headers,
              rows: component.data.rows
              rows: component.data.rows,
            };
            break;
          case 'fileUpload':
            componentData = component.data.fileList.map(file => {
              console.log('fileUpload fileUpload fileUpload',file)
          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 => {
          case "imageUpload":
            componentData = component.data.imageList.map((image) => {
              if (image.url && image.url.startsWith(prefix)) {
                return { ...image, url: image.url.substring(prefix.length) };
              }
@@ -342,11 +446,11 @@
        return {
          type: component.type,
          data: componentData
          data: componentData,
        };
      });
      this.$emit('submit', data);
      this.$emit("submit", data);
    },
    confirmAddRow(formData) {
      // if (!this.editable) return;
@@ -359,19 +463,15 @@
      // 调试输出
      if (isEdit) {
        // Vue无法检测到对象或数组深层属性的变化,使用Vue.set来确保响应式
        this.$set(
          this.components[idx].data.rows,
          rowIndex,
          {
            ...processedData,
            updateTime: new Date().toLocaleString()
          }
        );
        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()
          updateTime: new Date().toLocaleString(),
        });
      }
@@ -402,24 +502,24 @@
      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.components[idx].data.rows.forEach((row) => {
        if (typeof row === "object" && row !== null) {
          if (data.type === "user") {
            this.$set(row, data.name, []);
          } else {
            this.$set(row, data.name, '');
            this.$set(row, data.name, "");
          }
        } else {
          const newRow = {};
          this.components[idx].data.headers.forEach(header => {
          this.components[idx].data.headers.forEach((header) => {
            if (header.name === data.name) {
              if (header.type === 'user') {
              if (header.type === "user") {
                newRow[header.name] = [];
              } else {
                newRow[header.name] = '';
                newRow[header.name] = "";
              }
            } else {
              newRow[header.name] = row[header.name] || '';
              newRow[header.name] = row[header.name] || "";
            }
          });
          const rowIndex = this.components[idx].data.rows.indexOf(row);
@@ -473,7 +573,7 @@
          this.$message.success("删除成功");
          this.emitUpdate();
        })
        .catch(() => { });
        .catch(() => {});
    },
    handleFileChange(idx, fileList) {
      if (!this.editable) return;
@@ -500,12 +600,12 @@
      this.emitUpdate();
    },
    beforeImageUpload(file) {
      const isJPG = file.type === 'image/jpeg';
      const isPNG = file.type === 'image/png';
      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 格式!');
        this.$message.error("上传图片只能是 JPG 或 PNG 格式!");
        return false;
      }
      // if (!isLt2M) {
@@ -525,11 +625,10 @@
    emitUpdate() {
      // 先创建新对象,这有助于触发更新
      const updatedComponents = JSON.parse(JSON.stringify(this.components));
      this.$emit('update:dataSource', updatedComponents);
      this.$emit("update:dataSource", updatedComponents);
    },
  },
  computed: {
  },
  computed: {},
};
</script>
laboratory/src/utils/utils.js
@@ -81,5 +81,5 @@
export function getAllocateIp() {
  const userInfo = JSON.parse(sessionStorage.getItem("userInfo") || "{}");
  return userInfo.allocateIp ? userInfo.userInfo : apiConfig.showImgUrl;
  return userInfo.allocateIp ? userInfo.allocateIp +'/images/' : apiConfig.showImgUrl;
}
laboratory/src/views/dataManagement/sampleRecordList/components/sampleDialog.vue
@@ -146,7 +146,7 @@
<script>
import { updateRecordOperation } from '../service'
import { getFullUrl } from '@/utils/utils';
import { getFullUrl,getAllocateIp } from '@/utils/utils';
import apiConfig from '@/utils/baseurl';
export default {
@@ -271,7 +271,8 @@
          // 处理图片路径为相对路径
          let pictures = this.form.pictures;
          if (pictures) {
            const prefix = apiConfig.showImgUrl;
            // const prefix = apiConfig.showImgUrl;
            const prefix = getAllocateIp();
            pictures = pictures.split(',').map(url => {
              return url.startsWith(prefix) ? url.substring(prefix.length) : url;
            }).join(',');