From 527efb36f35b471710e445972673abff45bacdac Mon Sep 17 00:00:00 2001
From: 董国庆 <364620639@qq.com>
Date: 星期五, 12 九月 2025 17:36:09 +0800
Subject: [PATCH] 401跳转登录
---
laboratory/src/components/DynamicComponent/index.vue | 589 ++++++++++++++++++++++++++++++++++++++++++++--------------
1 files changed, 442 insertions(+), 147 deletions(-)
diff --git a/laboratory/src/components/DynamicComponent/index.vue b/laboratory/src/components/DynamicComponent/index.vue
index 8c69c70..54bfc79 100644
--- a/laboratory/src/components/DynamicComponent/index.vue
+++ b/laboratory/src/components/DynamicComponent/index.vue
@@ -5,6 +5,7 @@
<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"
@@ -15,6 +16,7 @@
<!-- 选择组件弹窗 -->
<AddComponentDialog
+ v-if="editable"
:visible="showAddDialog"
@confirm="addComponent"
@close="showAddDialog = false"
@@ -28,16 +30,18 @@
>
<!-- 富文本 -->
<div v-if="item.type == 'richText'">
- <AiEditor
+ <AiEditor
:ref="`editor_${item.id}`"
- v-model="item.data.content"
- height="200px"
- placeholder="请输入内容..."
+ :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">
+ <div v-if="editable" class="table-actions">
<el-button size="mini" @click="showTableHeaderDialog(idx)"
>添加表头</el-button
>
@@ -53,31 +57,58 @@
:total="null"
:height="null"
class="groupTable"
+ :key="item.id + '_' + JSON.stringify(item.data.rows).length"
>
- <!-- <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"
- />
+ >
+ <template slot-scope="scope">
+ <!-- 用户类型显示 -->
+ <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">
+ <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
>
@@ -88,32 +119,71 @@
<!-- 文件上传 -->
<div v-else-if="item.type == 'fileUpload'">
<el-upload
- action="#"
+ 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"
>
<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
+ v-if="editable"
src="@/assets/public/delete.png"
@click="removeComponent(idx)"
alt="删除"
@@ -122,26 +192,12 @@
</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"
+ :participants="participants"
@confirm="confirmAddHeader"
- ></addTableHeader>
+ >
+ </addTableHeader>
<addTableData
:visible.sync="rowDialog.visible"
:headerList="rowDialog.headers"
@@ -150,6 +206,19 @@
@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 +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,getAllocateIp } from "@/utils/utils";
export default {
name: "DynamicComponent",
@@ -174,9 +246,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 +285,117 @@
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 +403,93 @@
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;
+ const prefix = getAllocateIp();
+ 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,47 +497,43 @@
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.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);
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 +542,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 +556,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,105 +571,64 @@
.then(() => {
this.components[idx].data.rows.splice(rowIndex, 1);
this.$message.success("删除成功");
+ this.emitUpdate();
})
.catch(() => {});
},
- confirmAddRow(formData) {
- const { idx, rowIndex, isEdit } = this.rowDialog;
- if (isEdit) {
- // 编辑模式:替换原有行数据
- this.components[idx].data.rows.splice(rowIndex, 1, formData);
- } else {
- // 新增模式:添加新行数据
- this.components[idx].data.rows.push(formData);
- }
- this.rowDialog.visible = false;
- // 重置对话框数据
- this.rowDialog = {
- visible: false,
- idx: null,
- rowIndex: null,
- isEdit: false,
- headers: [],
- form: {},
- };
- },
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();
},
- // 获取所有组件数据
- getComponentsData() {
- // 整理数据,图片只保留url
- const submitData = this.components.map((item) => {
- if (item.type === "richText") {
- // 获取富文本编辑器的内容
- const editorRef = this.$refs[`editor_${item.id}`];
- return {
- ...item,
- data: {
- content: editorRef ? editorRef.getContent() : item.data.content
- }
- };
- }
- if (item.type === "imageUpload") {
- return {
- ...item,
- data: {
- imageList: item.data.imageList.map((img) => ({ url: img.url })),
- },
- };
- }
- return item;
- });
- return submitData;
- },
- // 验证所有组件数据
- validateComponents() {
- // 验证富文本编辑器
- const richTextValid = this.components.every(item => {
- if (item.type === 'richText') {
- const editorRef = this.$refs[`editor_${item.id}`];
- return editorRef && editorRef.getContent().trim() !== '';
- }
- return true;
- });
+ beforeImageUpload(file) {
+ const isJPG = file.type === "image/jpeg";
+ const isPNG = file.type === "image/png";
+ // const isLt2M = file.size / 1024 / 1024 < 2;
- if (!richTextValid) {
- this.$message.error('请填写所有富文本内容');
+ if (!isJPG && !isPNG) {
+ this.$message.error("上传图片只能是 JPG 或 PNG 格式!");
return false;
}
-
- // 验证表格数据
- const tableValid = this.components.every(item => {
- if (item.type === 'customTable') {
- return item.data.rows.length > 0;
- }
- return true;
- });
-
- if (!tableValid) {
- this.$message.error('请至少添加一行表格数据');
- return false;
- }
-
+ // if (!isLt2M) {
+ // this.$message.error('上传图片大小不能超过 2MB!');
+ // return false;
+ // }
+ this.imagePreviewVisible = true;
return true;
},
- // 提交数据
- submit() {
- if (this.validateComponents()) {
- const data = this.getComponentsData();
- this.$emit('submit', data);
- }
- }
+ 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>
@@ -403,9 +638,11 @@
padding: 20px;
margin-top: 37px;
}
-.has-title{
+
+.has-title {
margin-top: 0px !important;
}
+
.add-group {
display: flex;
align-items: center;
@@ -423,18 +660,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;
@@ -442,6 +708,7 @@
align-items: center;
justify-content: center;
flex-direction: column;
+
.upload-text {
font-weight: 400;
font-size: 14px;
@@ -450,6 +717,7 @@
margin-top: 13px;
}
}
+
.uploaf-notice {
font-weight: 400;
font-size: 14px;
@@ -457,17 +725,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