<template>
|
<div>
|
<div class="choose-material" :class="title ? '' : 'has-title'">
|
<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
|
>
|
</div>
|
|
<!-- 选择组件弹窗 -->
|
<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-if="item.type == 'richText'">
|
<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
|
>
|
</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"
|
>
|
<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" 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
|
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'">
|
<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="删除"
|
class="delete-icon"
|
/>
|
</div>
|
</div>
|
|
<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>
|
|
<script>
|
import AddComponentDialog from "../AddComponentDialog/index.vue";
|
import AiEditor from "../AiEditor/index.vue";
|
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",
|
components: {
|
AddComponentDialog,
|
AiEditor,
|
Table,
|
addTableHeader,
|
addTableData,
|
},
|
props: {
|
title: {
|
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: {
|
visible: false,
|
idx: null,
|
header: "",
|
},
|
rowDialog: {
|
visible: false,
|
idx: null,
|
rowIndex: null,
|
isEdit: false,
|
headers: [],
|
form: {},
|
},
|
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 = {};
|
if (type === "richText") data = { content: "" };
|
if (type === "customTable") data = { headers: [], rows: [] };
|
if (type === "fileUpload") data = { fileList: [] };
|
if (type === "imageUpload") data = { imageList: [] };
|
|
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;
|
this.tableHeaderDialog.idx = idx;
|
this.tableHeaderDialog.header = "";
|
},
|
confirmAddHeader(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, []);
|
} else {
|
this.$set(row, data.name, "");
|
}
|
} else {
|
const newRow = {};
|
this.components[idx].data.headers.forEach((header) => {
|
if (header.name === data.name) {
|
if (header.type === "user") {
|
newRow[header.name] = [];
|
} else {
|
newRow[header.name] = "";
|
}
|
} else {
|
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;
|
this.rowDialog.visible = true;
|
this.rowDialog.idx = idx;
|
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] = [];
|
} else {
|
this.rowDialog.form[header.name] = "";
|
}
|
});
|
},
|
handleEditRow(idx, rowIndex) {
|
this.rowDialog.visible = true;
|
this.rowDialog.idx = idx;
|
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: "取消",
|
type: "warning",
|
})
|
.then(() => {
|
this.components[idx].data.rows.splice(rowIndex, 1);
|
this.$message.success("删除成功");
|
this.emitUpdate();
|
})
|
.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) {
|
// 上传成功后设置真实 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>
|
|
<style scoped lang="less">
|
.choose-material {
|
background: #eff8fa;
|
padding: 20px;
|
margin-top: 37px;
|
}
|
|
.has-title {
|
margin-top: 0px !important;
|
}
|
|
.add-group {
|
display: flex;
|
align-items: center;
|
margin-bottom: 19px;
|
|
div {
|
color: #f56c6c;
|
}
|
|
span {
|
font-weight: 500;
|
font-size: 14px;
|
color: #222222;
|
line-height: 21px;
|
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;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
flex-direction: column;
|
|
.upload-text {
|
font-weight: 400;
|
font-size: 14px;
|
color: rgba(0, 0, 0, 0.85);
|
line-height: 22px;
|
margin-top: 13px;
|
}
|
}
|
|
.uploaf-notice {
|
font-weight: 400;
|
font-size: 14px;
|
color: rgba(0, 0, 0, 0.85);
|
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>
|