From 4ca375b2e3dae98d6ececc1b7b421a12f3fa8a26 Mon Sep 17 00:00:00 2001
From: 董国庆 <364620639@qq.com>
Date: 星期二, 01 七月 2025 09:00:38 +0800
Subject: [PATCH] 修改bug,验收通过
---
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