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