From 993e5fd593398926af72af660cb5ed6aba8e4e2b Mon Sep 17 00:00:00 2001 From: 13404089107 <puwei@sinata.cn> Date: 星期二, 20 五月 2025 16:43:04 +0800 Subject: [PATCH] 对接接口 --- laboratory/src/components/DynamicComponent/index.vue | 491 +++++++++++++++++++++++++++++++++++++++-------------- 1 files changed, 358 insertions(+), 133 deletions(-) diff --git a/laboratory/src/components/DynamicComponent/index.vue b/laboratory/src/components/DynamicComponent/index.vue index 08b2099..8faa19a 100644 --- a/laboratory/src/components/DynamicComponent/index.vue +++ b/laboratory/src/components/DynamicComponent/index.vue @@ -4,151 +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}`" - v-model="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'"> - <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="#" :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"> + <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" - @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> @@ -174,6 +119,22 @@ type: String, default: "", }, + participants: { + type: Array, + default: () => [] + }, + dataSource: { + type: Array, + default: () => [] + }, + editable: { + type: Boolean, + default: true + }, + dialogCanEdit: { + type: Boolean, + default: true + } }, data() { return { @@ -192,11 +153,102 @@ 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('请先选择实验调度'); + this.showAddDialog = false; + return; + } + } + this.showAddDialog = false; const id = Date.now() + Math.random(); let data = {}; @@ -204,12 +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; + 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 + }; + }); + + 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) { + // 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() + }); + } + 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; @@ -217,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, []); @@ -232,7 +358,6 @@ this.$set(row, data.name, ''); } } else { - // 如果行数据不是对象,转换为对象 const newRow = {}; this.components[idx].data.headers.forEach(header => { if (header.name === data.name) { @@ -245,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; @@ -266,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] = []; @@ -281,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: "取消", @@ -295,19 +419,60 @@ .then(() => { this.components[idx].data.rows.splice(rowIndex, 1); this.$message.success("删除成功"); + this.emitUpdate(); }) - .catch(() => {}); + .catch(() => { }); }, handleFileChange(idx, fileList) { + if (!this.editable) return; + + fileList = fileList.map(file => { + if (!file.url) { + file.url = 'https://picsum.photos/200/200'; + } + if (!file.name) { + file.name = '默认文件.txt'; + } + return file; + }); this.components[idx].data.fileList = fileList; + this.emitUpdate(); }, handleImageChange(idx, fileList) { + if (!this.editable) return; + + fileList = fileList.map(file => { + if (!file.url) { + file.url = 'https://picsum.photos/200/200'; + } + return file; + }); this.components[idx].data.imageList = fileList; + this.emitUpdate(); }, handleImageSuccess(res, file, fileList, idx) { - // 假设后端返回的图片地址在 res.url - file.url = res.url; + file.url = 'https://picsum.photos/200/200'; this.components[idx].data.imageList = fileList; + }, + 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; + } + return true; + }, + emitUpdate() { + // 先创建新对象,这有助于触发更新 + const updatedComponents = JSON.parse(JSON.stringify(this.components)); + this.$emit('update:dataSource', updatedComponents); }, }, }; @@ -319,9 +484,11 @@ padding: 20px; margin-top: 37px; } -.has-title{ + +.has-title { margin-top: 0px !important; } + .add-group { display: flex; align-items: center; @@ -339,18 +506,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 +554,7 @@ align-items: center; justify-content: center; flex-direction: column; + .upload-text { font-weight: 400; font-size: 14px; @@ -366,6 +563,7 @@ margin-top: 13px; } } + .uploaf-notice { font-weight: 400; font-size: 14px; @@ -373,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> -- Gitblit v1.7.1