This commit is contained in:
2026-02-28 17:34:25 +08:00
parent 4dfc178b29
commit 2fb5b8cc8f
6 changed files with 956 additions and 0 deletions

View File

@@ -0,0 +1,135 @@
<template>
<div class="comp-edit-form">
<div class="form">
<div v-for="(item, index) in formData" :key="index" class="form-item">
<div class="title">
<el-input
v-if="item.params"
v-model="item.params.title"
placeholder="请输入标题"
clearable
:disabled="disabled"
/>
</div>
<div class="key">
<el-input
v-if="mode === 'input'"
v-model="item.value"
placeholder="请输入"
clearable
:disabled="disabled"
/>
<el-input
v-else
v-model="item.key"
placeholder="请输入key值"
clearable
:disabled="disabled"
/>
</div>
<div v-if="mode === 'edit'" class="option">
<el-link type="danger" @click="delFun(index)">删除</el-link>
</div>
</div>
</div>
<div v-if="mode === 'edit'" class="add-btn">
<el-button :icon="CirclePlus" type="primary" size="small" @click="addFun">添加</el-button>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue';
import { CirclePlus } from '@element-plus/icons-vue';
import { cloneDeep } from 'lodash-es';
interface Props {
children: any;
mode: any;
disabled?: any;
}
const props = withDefaults(defineProps<Props>(), {
children: '',
mode: '',
disabled: false,
});
const emit = defineEmits(['update:children']);
const formData = ref<any>([]);
watch(
() => props.children,
(val: any, oldVal: any) => {
if (JSON.stringify(val) !== JSON.stringify(oldVal)) {
formData.value = cloneDeep(val) || [];
}
},
{ deep: true, immediate: true }
);
watch(
() => formData.value,
(val: any) => {
emit('update:children', val);
},
{
deep: true,
immediate: true,
}
);
const addFun = () => {
formData.value.push({
id: new Date().getTime(),
type: 'text',
key: '', // 报告生成对应key
value: '', // 内容
params: {
// 其他参数
title: '',
},
children: [], // 子节点
});
};
const delFun = (index: any) => {
formData.value.splice(index, 1);
};
</script>
<style lang="scss" scoped>
.comp-edit-form {
.form {
display: flex;
justify-content: space-between;
flex-wrap: wrap;
.form-item {
width: 48%;
display: flex;
justify-content: space-between;
padding: 5px 0;
.title {
flex: 1;
margin-right: 10px;
}
.key {
flex: 1;
margin-right: 10px;
}
.option {
display: flex;
align-items: center;
justify-content: center;
width: 30px;
}
}
}
.add-btn {
display: flex;
align-items: center;
justify-content: center;
padding-top: 10px;
}
}
</style>

View File

@@ -0,0 +1,325 @@
<template>
<div class="comp-edit-img">
<template v-if="mode === 'edit'">
<div v-for="(item, index) in Array(paramsData.colNum)" :key="index" class="pic">
<el-icon :size="100"><Picture /></el-icon>
<div class="title-item">{{ paramsData.title }}</div>
</div>
</template>
<div
v-else
ref="PasteContentRef"
class="paste-content"
:class="{ active: isFocus }"
:contenteditable="disabled ? false : contenteditable"
@click="clickFun"
@paste="pasteFun"
>
<template v-if="fileList.length > 0">
<el-row :gutter="20">
<el-col v-for="(item, index) in fileList" :key="index" :span="24 / paramsData.colNum">
<div class="img-item">
<div class="img">
<img :src="item.src" />
</div>
<div class="tip">
<el-input
class="img-tip-inp"
v-model="item.title"
:placeholder="params.placeholder || '请输入图例名称'"
clearable
@input="inputFun"
/>
</div>
<div class="del-btn" @click.stop="delFun(index)">
<el-icon :size="22"><DeleteFilled /></el-icon>
</div>
</div>
</el-col>
</el-row>
</template>
<div v-else class="preview">
<div class="no-data">单击此处粘贴图片...</div>
<div class="placeholder">{{ paramsData.title }}</div>
</div>
</div>
<Dialog v-model="diaShow" diaTitle="图片设置" :width="400" @close="closeFun">
<div class="content">
<el-form label-width="auto">
<el-form-item label="图片key">
<el-input v-model="titleKey" placeholder="请输入" clearable />
</el-form-item>
<el-form-item label="默认名称">
<el-input v-model="paramsData.title" placeholder="请输入" clearable />
</el-form-item>
<el-form-item label="输入提示">
<el-input v-model="paramsData.placeholder" placeholder="请输入" clearable />
</el-form-item>
<el-form-item label="展示列数">
<el-input-number
v-model="paramsData.colNum"
:min="1"
:max="4"
:step="1"
step-strictly
/>
</el-form-item>
</el-form>
</div>
<template #footer>
<div>
<el-button type="primary" @click="closeFun">确定</el-button>
</div>
</template>
</Dialog>
</div>
</template>
<script setup lang="ts">
import { ref, watch, onMounted, onBeforeUnmount, nextTick } from 'vue';
import { Picture, DeleteFilled } from '@element-plus/icons-vue';
import Dialog from '@/components/common/dialog/index.vue';
import { cloneDeep } from 'lodash-es';
const emit = defineEmits(['update:keyValue', 'update:value', 'update:params']);
interface Props {
keyValue: any;
value: any;
params: any;
mode: string;
disabled?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
keyValue: '',
value: [],
params: {},
mode: '',
disabled: false,
});
const diaShow = ref(false);
const titleKey = ref(props.keyValue);
const fileList = ref<any>([]);
const isFocus = ref(false);
const PasteContentRef = ref();
const contenteditable = ref(true);
const paramsData = ref(cloneDeep(props.params));
if (!paramsData.value.colNum) {
paramsData.value.colNum = 1;
}
emit('update:value', fileList.value);
emit('update:params', paramsData.value);
watch(
() => titleKey.value,
(val: any) => {
emit('update:keyValue', val);
},
{ deep: true }
);
watch(
() => paramsData.value,
(val: any) => {
emit('update:params', val);
},
{
deep: true,
}
);
watch(
() => props.value,
(val: any) => {
fileList.value = cloneDeep(val) || [];
},
{ deep: true, immediate: true }
);
onMounted(() => {
document.addEventListener('click', focusFun);
});
onBeforeUnmount(() => {
document.removeEventListener('click', focusFun);
});
const clickFun = () => {
if (props.disabled) {
return;
}
isFocus.value = true;
};
const focusFun = (event: any) => {
if (props.disabled) {
return;
}
if (PasteContentRef.value && !PasteContentRef.value.contains(event.target)) {
isFocus.value = false;
}
};
const delFun = (index: any) => {
contenteditable.value = false;
nextTick(() => {
PasteContentRef.value.click();
PasteContentRef.value.focus();
fileList.value.splice(index, 1);
contenteditable.value = true;
emit('update:value', fileList.value);
});
};
const pasteFun = (event: any) => {
if (props.disabled) {
return;
}
const dom = event.target;
const tagName = dom.tagName;
if (tagName.toLowerCase() !== 'input') {
event.preventDefault();
}
const win: any = window;
const clipboardData = event.clipboardData || win.clipboardData;
const items = clipboardData.items;
if (!items) return;
for (let i = 0; i < items.length; i++) {
const item = items[i];
if (item.kind === 'file' && item.type.startsWith('image/')) {
const file = item.getAsFile();
const reader = new FileReader();
reader.onload = () => {
const base64 = reader.result;
fileList.value.push({
src: base64,
title: props.params.title,
});
emit('update:value', fileList.value);
};
reader.readAsDataURL(file);
break;
}
}
};
const inputFun = () => {
emit('update:value', fileList.value);
};
const openFun = () => {
diaShow.value = true;
};
const closeFun = () => {
diaShow.value = false;
};
defineExpose({
openFun,
});
</script>
<style lang="scss">
.comp-edit-img {
.img-tip-inp {
.el-input__inner {
text-align: center;
}
}
}
</style>
<style lang="scss" scoped>
.comp-edit-img {
position: relative;
margin-bottom: 10px;
display: flex;
align-items: center;
justify-content: flex-start;
border-radius: 4px;
padding: 20px 10px 10px;
border: solid 1px var(--el-border-color);
.pic {
width: 100%;
height: 200px;
border-radius: 4px;
background-color: var(--el-bg-color);
color: var(--el-text-color-placeholder);
display: flex;
align-items: center;
justify-content: center;
align-content: center;
flex-wrap: wrap;
.title-item {
width: 100%;
text-align: center;
}
}
.paste-content {
padding: 20px;
width: 100%;
min-height: 200px;
background-color: var(--el-bg-color);
border-radius: 4px;
border: dashed 4px var(--el-bg-color);
&.active {
border-color: var(--el-color-primary);
}
.img-item {
position: relative;
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin-bottom: 20px;
&:hover {
.del-btn {
opacity: 1;
}
}
.img {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
margin-bottom: 10px;
img {
max-width: 100%;
}
}
.tip {
width: 100%;
max-width: 300px;
}
.del-btn {
opacity: 0;
position: absolute;
top: 0;
right: 0;
color: var(--el-color-danger);
cursor: pointer;
}
}
.preview {
width: 100%;
height: 148px;
display: flex;
flex-direction: column;
justify-content: space-between;
align-content: space-between;
.no-data {
color: var(--el-text-color-placeholder);
}
.placeholder {
text-align: center;
color: var(--el-text-color-primary);
}
}
}
}
</style>

View File

@@ -0,0 +1,191 @@
<template>
<div class="comp-edit-table">
<el-table :data="tableData">
<el-table-column
v-for="(item, index) in head"
:key="index"
:prop="item.key"
:label="item.title"
>
<template #default="scope">
<el-input v-model="scope.row[item.key]" clearable @input="inputFun" />
</template>
</el-table-column>
<el-table-column v-if="mode === 'input'" prop="actions" label="操作" fixed="right" width="60">
<template #default="scope">
<el-link type="danger" @click="delDataFun(scope.$index)">删除</el-link>
</template>
</el-table-column>
</el-table>
<div v-if="mode === 'input' && !disabled" class="add-column">
<el-button :icon="CirclePlus" type="primary" size="small" @click="addDataFun">添加</el-button>
</div>
<Dialog v-model="diaShow" diaTitle="表格设置" :width="500" @close="closeFun">
<div class="content">
<el-form label-width="auto">
<el-form-item label="表格key">
<el-input v-model="titleKey" placeholder="请输入" clearable />
</el-form-item>
<el-form-item label="表格类型">
<el-select v-model="paramsData.tableType">
<el-option label="指标" value="performance" />
<el-option label="普通" value="normal" />
</el-select>
</el-form-item>
</el-form>
<div class="table">
<el-table :data="headData">
<el-table-column prop="title" label="表头名称">
<template #default="scope">
<el-input v-model="scope.row.title" clearable />
</template>
</el-table-column>
<el-table-column prop="key" label="表头key值">
<template #default="scope">
<el-input v-model="scope.row.key" clearable />
</template>
</el-table-column>
<el-table-column prop="actions" label="操作" width="60">
<template #default="scope">
<el-link type="danger" @click="delHeadFun(scope.$index)">删除</el-link>
</template>
</el-table-column>
</el-table>
</div>
</div>
<template #footer>
<div>
<el-button @click="addFun">新增列头</el-button>
<el-button type="primary" @click="saveFun">确定</el-button>
</div>
</template>
</Dialog>
</div>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue';
import { CirclePlus } from '@element-plus/icons-vue';
import Dialog from '@/components/common/dialog/index.vue';
import { cloneDeep } from 'lodash-es';
const emit = defineEmits(['update:keyValue', 'update:value', 'update:params', 'update:head']);
interface Props {
keyValue: any;
value: any;
params: any;
mode: string;
head?: any;
disabled?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
keyValue: '',
value: [],
params: {},
mode: '',
head: [],
disabled: false,
});
const diaShow = ref(false);
const titleKey = ref(props.keyValue);
const tableData = ref<any>([]);
const headData = ref<any>(cloneDeep(props.head));
const paramsData = ref(cloneDeep(props.params));
emit('update:value', tableData.value);
emit('update:head', headData.value);
emit('update:params', paramsData.value);
watch(
() => titleKey.value,
(val: any) => {
emit('update:keyValue', val);
},
{ deep: true }
);
watch(
() => paramsData.value,
(val: any) => {
emit('update:params', val);
},
{
deep: true,
}
);
watch(
() => props.head,
(val: any) => {
headData.value = cloneDeep(val) || [];
},
{ deep: true, immediate: true }
);
watch(
() => props.value,
(val: any) => {
tableData.value = cloneDeep(val) || [];
},
{ deep: true, immediate: true }
);
const inputFun = () => {
emit('update:value', tableData.value);
};
const addFun = () => {
headData.value.push({
title: '',
key: '',
});
};
const addDataFun = () => {
tableData.value.push({});
emit('update:value', tableData.value);
};
const delDataFun = (index: any) => {
tableData.value.splice(index, 1);
emit('update:value', tableData.value);
};
const delHeadFun = (index: any) => {
headData.value.splice(index, 1);
};
const saveFun = () => {
emit('update:head', headData.value);
closeFun();
};
const openFun = () => {
diaShow.value = true;
};
const closeFun = () => {
diaShow.value = false;
};
defineExpose({
openFun,
});
</script>
<style lang="scss" scoped>
.comp-edit-table {
position: relative;
border-radius: 4px;
padding: 20px 10px 10px;
border: solid 1px var(--el-border-color);
}
.add-column {
display: flex;
align-items: center;
justify-content: center;
padding-top: 10px;
}
</style>

View File

@@ -0,0 +1,53 @@
<template>
<div class="comp-edit-text">
<el-input
v-model="textData"
type="textarea"
:placeholder="placeholderFun('请输入文本')"
:disabled="disabled"
/>
</div>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue';
interface Props {
keyValue: any;
value: any;
mode: any;
disabled?: any;
}
const props = withDefaults(defineProps<Props>(), {
keyValue: '',
value: '',
mode: '',
disabled: false,
});
const emit = defineEmits(['update:keyValue', 'update:value']);
const textData = ref('');
watch(
() => props.value,
(val: any) => {
textData.value = val;
}
);
watch(
() => textData.value,
(val: any) => {
if (props.mode === 'edit') {
emit('update:keyValue', val);
} else {
emit('update:value', val);
}
}
);
const placeholderFun = (text: string) => {
return `${text}${props.mode === 'edit' ? 'key' : ''}`;
};
</script>

View File

@@ -0,0 +1,48 @@
<template>
<div class="comp-edit-title">
<el-input
v-if="mode === 'edit'"
v-model="titleData"
type="input"
placeholder="请输入标题"
clearable
:disabled="disabled"
/>
<div v-else class="title">{{ titleData }}</div>
</div>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue';
interface Props {
value: any;
mode: any;
disabled?: any;
}
const props = withDefaults(defineProps<Props>(), {
value: '',
mode: '',
disabled: false,
});
const emit = defineEmits(['update:value']);
const titleData = ref(props.value);
watch(
() => titleData.value,
(val: any) => {
emit('update:value', val);
}
);
</script>
<style lang="scss" scoped>
.comp-edit-title {
.title {
font-size: 14px;
color: var(--el-text-color-secondary);
}
}
</style>

View File

@@ -0,0 +1,204 @@
<template>
<div class="comp-report">
<div class="content">
<div class="preview">
<KkfileView :fileId="fileId" />
</div>
<div class="edit">
<EditItem v-model:data="documentDataList" :mode="mode" :preview="preview" />
<div v-if="mode === 'edit'" class="add-paragraph">
<el-button :icon="DocumentAdd" type="primary" size="small" @click="addParagraphFun">
新增章节
</el-button>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue';
import EditItem from './editItem.vue';
import { DocumentAdd } from '@element-plus/icons-vue';
import KkfileView from '@/components/common/filePreview/kkfileView.vue';
import { cloneDeep } from 'lodash-es';
import { ElMessage } from 'element-plus';
interface Props {
data: any;
valueList?: any;
fileId: any;
mode: string;
preview?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
data: [],
valueList: [],
fileId: '',
mode: 'input', // input输入模式 edit编辑模板模式
preview: false,
});
const emit = defineEmits(['update:data']);
const documentDataList = ref<any>([]);
watch(
() => props.data,
(val: any, oldVal: any) => {
if (JSON.stringify(val) !== JSON.stringify(oldVal)) {
documentDataList.value = val;
}
},
{ deep: true, immediate: true }
);
watch(
() => documentDataList.value,
(val: any) => {
emit('update:data', val);
},
{ deep: true }
);
const formatDataFun = (data: any) => {
data.forEach((item: any) => {
const matchData = props.valueList?.find((i: any) => i.key === item.key);
if (item.key && matchData) {
// 更具key值将历史数据的value赋值给模板
item.value = matchData.value;
}
if (item.children?.length > 0) {
formatDataFun(item.children);
}
});
};
watch(
() => props.valueList,
() => {
const formatData = cloneDeep(props.data);
formatDataFun(formatData);
emit('update:data', formatData);
},
{ deep: true, immediate: true }
);
// 新增段落/小结
const addParagraphFun = () => {
const data: any = {
id: new Date().getTime(),
type: 'paragraph', // 类型
key: '', // 报告生成对应key
value: '', // 内容
params: {}, // 其他参数
children: [], // 子节点
};
documentDataList.value.push(data);
};
const formatFun = (data: any) => {
data.forEach((item: any, index: number) => {
if (item.type !== 'form') {
const itemData: any = {
type: item.type,
key: item.key,
value: item.value,
params: item.params,
};
if (item.type === 'img') {
itemData.value.forEach((val: any, valIndex: number) => {
let picName = '';
if (val.title) {
picName = `${val.title.replace(/\s/g, '')}_${new Date().getTime()}_${index + 1}_${valIndex + 1}`;
} else {
picName = `图片_${new Date().getTime()}_${index + 1}_${valIndex + 1}`;
}
val.picName = picName;
});
}
if (item.type === 'table') {
itemData.head = item.head.map((i: any) => i.title);
}
formatData.value.push(itemData);
if (item.children?.length > 0) {
formatFun(item.children);
}
} else {
item.children.forEach((val: any) => {
const valData = {
type: val.type,
key: val.key,
value: val.value,
params: val.params,
};
formatData.value.push(valData);
});
}
});
};
const formatData = ref<any>([]);
const getFormatDataFun = () => {
formatData.value = [];
formatFun(documentDataList.value);
return formatData.value;
};
const checkKeysFun = () => {
const dataList = getFormatDataFun();
const keys: any = [];
let repeatKey = '';
dataList.some((item: any) => {
if (item.key) {
if (keys.includes(item.key)) {
repeatKey = item.key;
return true;
} else {
keys.push(item.key);
}
}
});
if (repeatKey) {
ElMessage.warning(`${repeatKey}重复,请更改后再提交`);
return false;
} else {
return true;
}
};
defineExpose({
getFormatDataFun,
checkKeysFun,
});
</script>
<style lang="scss" scoped>
.comp-report {
width: 100%;
height: 100%;
.content {
display: flex;
width: 100%;
height: 100%;
.preview {
width: 50%;
height: 100%;
padding: 20px;
overflow-y: auto;
}
.edit {
width: 50%;
height: 100%;
padding: 20px;
overflow-y: auto;
}
.add-paragraph {
margin-top: 10px;
display: flex;
align-items: center;
justify-content: center;
}
}
}
</style>