提交 faa6ac77 authored 作者: 王鹏飞's avatar 王鹏飞

chore: 优化营销策划

上级 c6b8711b
...@@ -9,6 +9,7 @@ ...@@ -9,6 +9,7 @@
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"@chuangkit/chuangkit-design": "^2.0.5", "@chuangkit/chuangkit-design": "^2.0.5",
"@dagrejs/dagre": "^1.1.3",
"@element-plus/icons-vue": "^2.3.1", "@element-plus/icons-vue": "^2.3.1",
"@fortaine/fetch-event-source": "^3.0.6", "@fortaine/fetch-event-source": "^3.0.6",
"@tinymce/tinymce-vue": "^5.0.1", "@tinymce/tinymce-vue": "^5.0.1",
...@@ -598,6 +599,24 @@ ...@@ -598,6 +599,24 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/@dagrejs/dagre": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@dagrejs/dagre/-/dagre-1.1.3.tgz",
"integrity": "sha512-umT7fBPECI4zgxxXW07H3vJN7W1WZcnBjk613eOEAKcwoFrYNyMZO+1SHmoC8zPZWR18DquK2wRUp9VHUE+94g==",
"license": "MIT",
"dependencies": {
"@dagrejs/graphlib": "2.2.2"
}
},
"node_modules/@dagrejs/graphlib": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/@dagrejs/graphlib/-/graphlib-2.2.2.tgz",
"integrity": "sha512-CbyGpCDKsiTg/wuk79S7Muoj8mghDGAESWGxcSyhHX5jD35vYMBZochYVFzlHxynpE9unpu6O+4ZuhrLxASsOg==",
"license": "MIT",
"engines": {
"node": ">17.0.0"
}
},
"node_modules/@element-plus/icons-vue": { "node_modules/@element-plus/icons-vue": {
"version": "2.3.1", "version": "2.3.1",
"resolved": "https://registry.npmjs.org/@element-plus/icons-vue/-/icons-vue-2.3.1.tgz", "resolved": "https://registry.npmjs.org/@element-plus/icons-vue/-/icons-vue-2.3.1.tgz",
......
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
}, },
"dependencies": { "dependencies": {
"@chuangkit/chuangkit-design": "^2.0.5", "@chuangkit/chuangkit-design": "^2.0.5",
"@dagrejs/dagre": "^1.1.3",
"@element-plus/icons-vue": "^2.3.1", "@element-plus/icons-vue": "^2.3.1",
"@fortaine/fetch-event-source": "^3.0.6", "@fortaine/fetch-event-source": "^3.0.6",
"@tinymce/tinymce-vue": "^5.0.1", "@tinymce/tinymce-vue": "^5.0.1",
......
...@@ -91,3 +91,8 @@ textarea:focus { ...@@ -91,3 +91,8 @@ textarea:focus {
.info tr:last-child td { .info tr:last-child td {
padding-bottom: 0 !important; padding-bottom: 0 !important;
} }
.el-button a {
margin: -8px -15px;
padding: 8px 15px;
}
...@@ -156,14 +156,15 @@ const handlePreview: UploadProps['onPreview'] = uploadFile => { ...@@ -156,14 +156,15 @@ const handlePreview: UploadProps['onPreview'] = uploadFile => {
overflow: hidden; overflow: hidden;
} }
.avatar-uploader { .avatar-uploader {
width: 178px; width: 180px;
height: 178px; height: 180px;
border: 1px dashed var(--el-border-color); border: 1px dashed var(--el-border-color);
border-radius: 6px; border-radius: 6px;
cursor: pointer; cursor: pointer;
position: relative; position: relative;
overflow: hidden; overflow: hidden;
transition: var(--el-transition-duration-fast); transition: var(--el-transition-duration-fast);
box-sizing: border-box;
.el-image { .el-image {
width: 100%; width: 100%;
height: 100%; height: 100%;
...@@ -179,4 +180,7 @@ const handlePreview: UploadProps['onPreview'] = uploadFile => { ...@@ -179,4 +180,7 @@ const handlePreview: UploadProps['onPreview'] = uploadFile => {
height: 100%; height: 100%;
text-align: center; text-align: center;
} }
.el-upload__tip:empty {
display: none;
}
</style> </style>
...@@ -66,26 +66,36 @@ async function generateImage() { ...@@ -66,26 +66,36 @@ async function generateImage() {
async function generatePdf() { async function generatePdf() {
// const blob = await toBlob(reportRef.value, { width: 1000 }) // const blob = await toBlob(reportRef.value, { width: 1000 })
// saveAs(blob, '营销策划报告.png') // saveAs(blob, '营销策划报告.png')
const canvas = await toCanvas(reportRef.value) const canvas = await toCanvas(reportRef.value, { width: 1000 })
const imgData = canvas.toDataURL('image/png') const imgData = canvas.toDataURL('image/png')
const pdf = new jsPDF('p', 'px', 'a4')
const pdfWidth = pdf.internal.pageSize.getWidth()
const pdfHeight = pdf.internal.pageSize.getHeight()
const canvasWidth = canvas.width const canvasWidth = canvas.width
const canvasHeight = canvas.height const canvasHeight = canvas.height
const imgWidth = pdfWidth const imgWidth = 595 // 设置 PDF 页面的宽度为 595 像素
const imgHeight = (pdfWidth / canvasWidth) * canvasHeight const imgHeight = (canvasHeight / canvasWidth) * imgWidth
const totalPages = Math.ceil(imgHeight / pdfHeight) const pdf = new jsPDF({
orientation: 'portrait',
unit: 'px',
format: [imgWidth, imgHeight]
})
pdf.addImage(imgData, 'PNG', 0, 0, imgWidth, imgHeight)
for (let i = 0; i < totalPages; i++) { // const pdf = new jsPDF('p', 'px', 'a4')
if (i > 0) { // const pdfWidth = pdf.internal.pageSize.getWidth()
pdf.addPage() // const pdfHeight = pdf.internal.pageSize.getHeight()
}
const position = -i * pdfHeight // const canvasWidth = canvas.width
pdf.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight) // const canvasHeight = canvas.height
} // const imgWidth = pdfWidth
// const imgHeight = (pdfWidth / canvasWidth) * canvasHeight
// const totalPages = Math.ceil(imgHeight / pdfHeight)
// for (let i = 0; i < totalPages; i++) {
// if (i > 0) {
// pdf.addPage()
// }
// const position = -i * pdfHeight
// pdf.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight)
// }
pdf.save('营销策划报告.pdf') pdf.save('营销策划报告.pdf')
} }
...@@ -150,7 +160,7 @@ defineExpose({ generateImage, generatePdf }) ...@@ -150,7 +160,7 @@ defineExpose({ generateImage, generatePdf })
<li>指导教师:{{ teacherName }}</li> <li>指导教师:{{ teacherName }}</li>
</ul> </ul>
</div> </div>
<section id="step1" class="section" :class="{ hide: step !== 1 }" v-element-visibility="state => onElementVisibility(state, 1)"> <section id="step1" class="section" v-element-visibility="state => onElementVisibility(state, 1)">
<h2>一、营销背景</h2> <h2>一、营销背景</h2>
<h3>(一)当前业务面临的问题及挑战</h3> <h3>(一)当前业务面临的问题及挑战</h3>
<template v-for="(item, index) in objectiveStore.problems" :key="item.id"> <template v-for="(item, index) in objectiveStore.problems" :key="item.id">
...@@ -161,7 +171,7 @@ defineExpose({ generateImage, generatePdf }) ...@@ -161,7 +171,7 @@ defineExpose({ generateImage, generatePdf })
<p>营销目标{{ index + 1 }}{{ item.content }}</p> <p>营销目标{{ index + 1 }}{{ item.content }}</p>
</template> </template>
</section> </section>
<section id="step2" class="section" :class="{ hide: step !== 2 }" v-element-visibility="state => onElementVisibility(state, 2)"> <section id="step2" class="section" v-element-visibility="state => onElementVisibility(state, 2)">
<h2>二、营销渠道</h2> <h2>二、营销渠道</h2>
<p>本次营销选择的主要渠道为:</p> <p>本次营销选择的主要渠道为:</p>
<template v-for="(item, index) in connectionStore.activeConnections" :key="item.id"> <template v-for="(item, index) in connectionStore.activeConnections" :key="item.id">
...@@ -170,7 +180,7 @@ defineExpose({ generateImage, generatePdf }) ...@@ -170,7 +180,7 @@ defineExpose({ generateImage, generatePdf })
<p>选择该渠道的原因为:{{ item.content }}</p> <p>选择该渠道的原因为:{{ item.content }}</p>
</template> </template>
</section> </section>
<section id="step3" class="section" :class="{ hide: step !== 3 }" v-element-visibility="state => onElementVisibility(state, 3)"> <section id="step3" class="section" v-element-visibility="state => onElementVisibility(state, 3)">
<h2>三、用户分析</h2> <h2>三、用户分析</h2>
<h3>(一)用户性别分析</h3> <h3>(一)用户性别分析</h3>
<p>{{ memberStore.member.sex }}</p> <p>{{ memberStore.member.sex }}</p>
...@@ -183,7 +193,7 @@ defineExpose({ generateImage, generatePdf }) ...@@ -183,7 +193,7 @@ defineExpose({ generateImage, generatePdf })
<img :src="memberStore.member.source_file" /> <img :src="memberStore.member.source_file" />
</div> </div>
</section> </section>
<section id="step4" class="section" :class="{ hide: step !== 4 }" v-element-visibility="state => onElementVisibility(state, 4)"> <section id="step4" class="section" v-element-visibility="state => onElementVisibility(state, 4)">
<h2>四、用户标签体系设计</h2> <h2>四、用户标签体系设计</h2>
<template v-for="(item, index) in labelStore.treeLabels" :key="item.id"> <template v-for="(item, index) in labelStore.treeLabels" :key="item.id">
<h3>{{ numberToChinese(index + 1) }}{{ item.name }}</h3> <h3>{{ numberToChinese(index + 1) }}{{ item.name }}</h3>
...@@ -195,7 +205,7 @@ defineExpose({ generateImage, generatePdf }) ...@@ -195,7 +205,7 @@ defineExpose({ generateImage, generatePdf })
</template> </template>
</template> </template>
</section> </section>
<section id="step5" class="section" :class="{ hide: step !== 5 }" v-element-visibility="state => onElementVisibility(state, 5)"> <section id="step5" class="section" v-element-visibility="state => onElementVisibility(state, 5)">
<h2>五、用户精准分群设计</h2> <h2>五、用户精准分群设计</h2>
<h3>(一)静态群组</h3> <h3>(一)静态群组</h3>
<p>本项目设计如下静态群组:</p> <p>本项目设计如下静态群组:</p>
...@@ -214,18 +224,18 @@ defineExpose({ generateImage, generatePdf }) ...@@ -214,18 +224,18 @@ defineExpose({ generateImage, generatePdf })
<p>设计该群组的原因是:{{ item.reason }}</p> <p>设计该群组的原因是:{{ item.reason }}</p>
</template> </template>
</section> </section>
<section id="step6" class="section" :class="{ hide: step !== 6 }" v-element-visibility="state => onElementVisibility(state, 6)"> <section id="step6" class="section" v-element-visibility="state => onElementVisibility(state, 6)">
<h2>六、自动化营销旅程设计</h2> <h2>六、自动化营销旅程设计</h2>
<h3>(一)一级流程</h3> <h3>(一)一级流程</h3>
<p>本项目设计一级流程图如下。</p> <p>本项目设计一级流程图如下。</p>
<Flow <Flow
id="report-flow-1"
:nodes="tripStore.nodes" :nodes="tripStore.nodes"
:edges="tripStore.edges" :edges="tripStore.edges"
:zoom-on-scroll="false" :zoom-on-scroll="false"
:prevent-scrolling="false" :prevent-scrolling="false"
:nodes-draggable="false" disabled
:nodes-connectable="false" style="height: 200px; margin: 20px 0"></Flow>
style="height: 200px"></Flow>
<p>相关节点设计说明如下:</p> <p>相关节点设计说明如下:</p>
<template v-for="(item, index) in tripStore.nodes" :key="item.id"> <template v-for="(item, index) in tripStore.nodes" :key="item.id">
<h4>{{ index + 1 }}{{ item.data.label || item.label }}节点</h4> <h4>{{ index + 1 }}{{ item.data.label || item.label }}节点</h4>
...@@ -248,9 +258,9 @@ defineExpose({ generateImage, generatePdf }) ...@@ -248,9 +258,9 @@ defineExpose({ generateImage, generatePdf })
:edges="item.data.edges" :edges="item.data.edges"
:zoom-on-scroll="false" :zoom-on-scroll="false"
:prevent-scrolling="false" :prevent-scrolling="false"
:nodes-draggable="false" disabled
:nodes-connectable="false" style="height: 200px; margin: 20px 0"
style="height: 200px"></Flow> v-if="item.data.nodes?.length"></Flow>
<p>该二级流程图节点说明如下:</p> <p>该二级流程图节点说明如下:</p>
<template v-for="(item, index) in item.data.nodes" :key="item.id"> <template v-for="(item, index) in item.data.nodes" :key="item.id">
<h4>{{ index + 1 }}{{ item.data.label || item.label }}节点</h4> <h4>{{ index + 1 }}{{ item.data.label || item.label }}节点</h4>
...@@ -267,7 +277,7 @@ defineExpose({ generateImage, generatePdf }) ...@@ -267,7 +277,7 @@ defineExpose({ generateImage, generatePdf })
</template> </template>
</template> </template>
</section> </section>
<section id="step7" class="section" :class="{ hide: step !== 7 }" v-element-visibility="state => onElementVisibility(state, 7)"> <section id="step7" class="section" v-element-visibility="state => onElementVisibility(state, 7)">
<h2>七、营销物料设计</h2> <h2>七、营销物料设计</h2>
<p>本项目设计如下营销物料。</p> <p>本项目设计如下营销物料。</p>
<AppList v-bind="listOptions" style="margin: 10px"></AppList> <AppList v-bind="listOptions" style="margin: 10px"></AppList>
...@@ -346,6 +356,7 @@ defineExpose({ generateImage, generatePdf }) ...@@ -346,6 +356,7 @@ defineExpose({ generateImage, generatePdf })
} }
img { img {
max-width: 90%; max-width: 90%;
margin: 20px 0;
} }
.market-report-step { .market-report-step {
position: fixed; position: fixed;
...@@ -364,7 +375,8 @@ defineExpose({ generateImage, generatePdf }) ...@@ -364,7 +375,8 @@ defineExpose({ generateImage, generatePdf })
cursor: pointer; cursor: pointer;
background-color: #fff; background-color: #fff;
&.is-active { &.is-active {
background-color: rgb(189, 248, 180); color: #fff;
background-color: var(--main-color);
border: none; border: none;
} }
} }
......
...@@ -46,13 +46,7 @@ async function handleNext() { ...@@ -46,13 +46,7 @@ async function handleNext() {
<ul> <ul>
<li v-for="(item, index) in objectiveStore.problems" :key="item.id"> <li v-for="(item, index) in objectiveStore.problems" :key="item.id">
<p>问题/挑战:</p> <p>问题/挑战:</p>
<el-input <el-input type="textarea" v-model="item.content" show-word-limit maxlength="200" :rows="4" :disabled="isCheck"></el-input>
type="textarea"
v-model="item.content"
show-word-limit
maxlength="200"
:autosize="{ minRows: 3, maxRows: 6 }"
:disabled="isCheck"></el-input>
<el-button <el-button
type="primary" type="primary"
:icon="Plus" :icon="Plus"
...@@ -68,13 +62,7 @@ async function handleNext() { ...@@ -68,13 +62,7 @@ async function handleNext() {
<ul> <ul>
<li v-for="(item, index) in objectiveStore.objectives" :key="item.id"> <li v-for="(item, index) in objectiveStore.objectives" :key="item.id">
<p>营销目标:</p> <p>营销目标:</p>
<el-input <el-input type="textarea" v-model="item.content" show-word-limit maxlength="200" :rows="4" :disabled="isCheck"></el-input>
type="textarea"
v-model="item.content"
show-word-limit
maxlength="200"
:autosize="{ minRows: 3, maxRows: 6 }"
:disabled="isCheck"></el-input>
<el-button <el-button
type="primary" type="primary"
:icon="Plus" :icon="Plus"
......
...@@ -59,13 +59,7 @@ async function handleNext() { ...@@ -59,13 +59,7 @@ async function handleNext() {
</div> </div>
<div v-show="item.active"> <div v-show="item.active">
<p>*选择原因</p> <p>*选择原因</p>
<el-input <el-input type="textarea" v-model="item.content" show-word-limit maxlength="100" :rows="3" :disabled="isCheck"></el-input>
type="textarea"
v-model="item.content"
show-word-limit
maxlength="100"
:autosize="{ minRows: 3, maxRows: 3 }"
:disabled="isCheck"></el-input>
</div> </div>
</div> </div>
</div> </div>
...@@ -92,6 +86,9 @@ async function handleNext() { ...@@ -92,6 +86,9 @@ async function handleNext() {
background-color: var(--main-color); background-color: var(--main-color);
} }
} }
p {
line-height: 30px;
}
} }
.connect-box { .connect-box {
padding: 10px; padding: 10px;
......
...@@ -57,33 +57,38 @@ async function handleNext() { ...@@ -57,33 +57,38 @@ async function handleNext() {
</div> </div>
<el-form label-width="150" label-suffix=":" :model="memberStore.member" :rules="rules" ref="formRef" :disabled="isCheck"> <el-form label-width="150" label-suffix=":" :model="memberStore.member" :rules="rules" ref="formRef" :disabled="isCheck">
<el-form-item label="用户性别分析" prop="sex"> <el-form-item label="用户性别分析" prop="sex">
<div style="width: 100%"> <div class="custom-form-item">
<el-input type="textarea" v-model="memberStore.member.sex" show-word-limit maxlength="200" :autosize="{ minRows: 3, maxRows: 6 }"></el-input> <div class="custom-form-item-left">
<AppUpload v-model="memberStore.member.sex_file" style="margin-top: 20px"></AppUpload> <el-input type="textarea" v-model="memberStore.member.sex" show-word-limit maxlength="200" :rows="5"></el-input>
</div>
<div class="custom-form-item-right">
<AppUpload v-model="memberStore.member.sex_file"></AppUpload>
</div>
</div> </div>
</el-form-item> </el-form-item>
<el-form-item label="用户数据来源分析" prop="source"> <el-form-item label="用户数据来源分析" prop="source">
<div style="width: 100%"> <div class="custom-form-item">
<el-input type="textarea" v-model="memberStore.member.source" show-word-limit maxlength="200" :autosize="{ minRows: 3, maxRows: 6 }"></el-input> <div class="custom-form-item-left">
<AppUpload v-model="memberStore.member.source_file" style="margin-top: 20px"></AppUpload> <el-input type="textarea" v-model="memberStore.member.source" show-word-limit maxlength="200" :rows="5"></el-input>
</div>
<div class="custom-form-item-right">
<AppUpload v-model="memberStore.member.source_file"></AppUpload>
</div>
</div> </div>
</el-form-item> </el-form-item>
<el-form-item label="用户属性分析"> <el-form-item label="用户属性分析">
<ul> <ul>
<li v-for="(item, index) in memberStore.member.attrs" :key="index"> <li v-for="(item, index) in memberStore.member.attrs" :key="index">
<div style="flex: 1"> <div class="custom-form-item">
<el-select-v2 v-model="item.attr_id" :options="memberAttrs" :props="{ label: 'name', value: 'id' }"></el-select-v2> <div class="custom-form-item-left">
<el-input <el-select-v2 v-model="item.attr_id" :options="memberAttrs" :props="{ label: 'name', value: 'id' }" style="width: 200px"></el-select-v2>
type="textarea" <el-input type="textarea" v-model="item.attr_content" show-word-limit maxlength="200" :rows="5" style="margin-top: 10px"> </el-input>
v-model="item.attr_content" </div>
show-word-limit <div class="custom-form-item-right">
maxlength="200" <AppUpload v-model="item.attr_file"></AppUpload>
:autosize="{ minRows: 3, maxRows: 6 }" <el-button type="primary" :icon="Minus" @click="handleRemove(item)"></el-button>
style="margin: 10px 0"> </div>
</el-input>
<AppUpload v-model="item.attr_file"></AppUpload>
</div> </div>
<el-button type="primary" :icon="Minus" @click="handleRemove(item)"></el-button>
</li> </li>
</ul> </ul>
<el-button type="primary" :icon="Plus" @click="handleAdd"></el-button> <el-button type="primary" :icon="Plus" @click="handleAdd"></el-button>
...@@ -114,4 +119,26 @@ li { ...@@ -114,4 +119,26 @@ li {
align-items: center; align-items: center;
column-gap: 20px; column-gap: 20px;
} }
.custom-form-item {
width: 100%;
display: flex;
align-items: flex-end;
gap: 20px;
:deep(.avatar-uploader) {
width: 115px;
height: 115px;
}
.uploader {
line-height: 0;
}
}
.custom-form-item-left {
flex: 1;
}
.custom-form-item-right {
width: 200px;
display: flex;
align-items: center;
justify-content: space-between;
}
</style> </style>
...@@ -51,13 +51,14 @@ async function handleNext() { ...@@ -51,13 +51,14 @@ async function handleNext() {
<div class="market-label"> <div class="market-label">
<div class="market-label-box" v-for="item in labelStore.treeLabels" :key="item.name"> <div class="market-label-box" v-for="item in labelStore.treeLabels" :key="item.name">
<h4>{{ item.name }}</h4> <h4>{{ item.name }}</h4>
<ul> <ul v-if="item.children?.length">
<li v-for="child in item.children" :key="child.id"> <li v-for="child in item.children" :key="child.id">
<p @click="handleView(child)">{{ child.name }}</p> <p @click="handleView(child)">{{ child.name }}</p>
<!-- <el-button type="primary" text size="small" @click="handleView(child)">查看</el-button> --> <!-- <el-button type="primary" text size="small" @click="handleView(child)">查看</el-button> -->
<el-button :icon="Minus" size="small" @click="handleRemove(child)" :disabled="isCheck"></el-button> <el-button :icon="Minus" size="small" @click="handleRemove(child)" :disabled="isCheck"></el-button>
</li> </li>
</ul> </ul>
<el-empty desc="暂无数据" v-else></el-empty>
<el-button type="primary" :icon="Plus" @click="handleAdd(item)" :disabled="isCheck"></el-button> <el-button type="primary" :icon="Plus" @click="handleAdd(item)" :disabled="isCheck"></el-button>
</div> </div>
</div> </div>
...@@ -83,6 +84,7 @@ async function handleNext() { ...@@ -83,6 +84,7 @@ async function handleNext() {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: space-between;
ul { ul {
width: 100%; width: 100%;
flex: 1; flex: 1;
......
...@@ -92,7 +92,7 @@ const handleSubmit = async () => { ...@@ -92,7 +92,7 @@ const handleSubmit = async () => {
</template> </template>
<el-form-item label="标签设置规则及说明" prop="desc"> <el-form-item label="标签设置规则及说明" prop="desc">
<el-input type="textarea" v-model="form.desc" show-word-limit maxlength="100" :autosize="{ minRows: 3, maxRows: 6 }"></el-input> <el-input type="textarea" v-model="form.desc" show-word-limit maxlength="100" :rows="4"></el-input>
</el-form-item> </el-form-item>
</el-form> </el-form>
<template #footer> <template #footer>
......
...@@ -23,9 +23,14 @@ const detail = ref({ ...@@ -23,9 +23,14 @@ const detail = ref({
three_quarters: '0' three_quarters: '0'
} }
}) })
// 1字符串 2 数字
const type = computed(() => {
return props.attrType == 2 || props.attrType == 3 ? 2 : 1
})
async function fetchInfo() { async function fetchInfo() {
const map = { 1: 1, 2: 2, 3: 2 } const res = await getMemberAttrAnalysis({ attr_id: props.attrId, attr_type: type.value })
const res = await getMemberAttrAnalysis({ attr_id: props.attrId, attr_type: map[props.attrType] })
detail.value = res.data detail.value = res.data
} }
watch(() => props.attrId, fetchInfo) watch(() => props.attrId, fetchInfo)
...@@ -52,7 +57,7 @@ const options = computed(() => { ...@@ -52,7 +57,7 @@ const options = computed(() => {
<template> <template>
<div> <div>
<el-form-item label="字段值分布"> <el-form-item label="字段值分布">
<template v-if="attrType != 1"> <template v-if="type === 2">
<div> <div>
<el-form-item label-width="auto" label="平均值">{{ detail.num_analysis.avg }}</el-form-item> <el-form-item label-width="auto" label="平均值">{{ detail.num_analysis.avg }}</el-form-item>
<el-form-item label-width="auto" label="最大值">{{ detail.num_analysis.max }}</el-form-item> <el-form-item label-width="auto" label="最大值">{{ detail.num_analysis.max }}</el-form-item>
......
...@@ -54,24 +54,30 @@ async function handleNext() { ...@@ -54,24 +54,30 @@ async function handleNext() {
</div> </div>
<div class="market-group"> <div class="market-group">
<div class="market-group-box"> <div class="market-group-box">
<h4>静态用户群组</h4> <div class="market-group-box-hd">
<ul> <h4>静态用户群组</h4>
<el-button type="primary" :icon="Plus" @click="handleAdd({ type: 1, type_name: '静态群组' })" :disabled="isCheck"></el-button>
</div>
<ul v-if="groupStore.staticGroups?.length">
<li v-for="(item, index) in groupStore.staticGroups" :key="index" @click="handleView(item)"> <li v-for="(item, index) in groupStore.staticGroups" :key="index" @click="handleView(item)">
<p>{{ item.name }}</p> <p>{{ item.name }}</p>
<el-icon @click.stop="handleRemove(item)" class="remove" v-if="!isCheck"><CircleClose /></el-icon> <el-icon @click.stop="handleRemove(item)" class="remove" v-if="!isCheck"><CircleClose /></el-icon>
</li> </li>
</ul> </ul>
<el-button type="primary" :icon="Plus" @click="handleAdd({ type: 1, type_name: '静态群组' })" :disabled="isCheck"></el-button> <el-empty desc="暂无数据" v-else></el-empty>
</div> </div>
<div class="market-group-box"> <div class="market-group-box">
<h4>动态用户群组</h4> <div class="market-group-box-hd">
<ul> <h4>动态用户群组</h4>
<el-button type="primary" :icon="Plus" @click="handleAdd({ type: 2, type_name: '动态群组' })" :disabled="isCheck"></el-button>
</div>
<ul v-if="groupStore.dynamicGroups?.length">
<li v-for="(item, index) in groupStore.dynamicGroups" :key="index" @click="handleView(item)"> <li v-for="(item, index) in groupStore.dynamicGroups" :key="index" @click="handleView(item)">
<p>{{ item.name }}</p> <p>{{ item.name }}</p>
<el-icon @click.stop="handleRemove(item)" class="remove" v-if="!isCheck"><CircleClose /></el-icon> <el-icon @click.stop="handleRemove(item)" class="remove" v-if="!isCheck"><CircleClose /></el-icon>
</li> </li>
</ul> </ul>
<el-button type="primary" :icon="Plus" @click="handleAdd({ type: 2, type_name: '动态群组' })" :disabled="isCheck"></el-button> <el-empty desc="暂无数据" v-else></el-empty>
</div> </div>
</div> </div>
<div class="market-step-footer"> <div class="market-step-footer">
...@@ -106,6 +112,8 @@ async function handleNext() { ...@@ -106,6 +112,8 @@ async function handleNext() {
justify-content: center; justify-content: center;
cursor: pointer; cursor: pointer;
border: 1px solid #ccc; border: 1px solid #ccc;
padding: 20px;
line-height: 24px;
.remove { .remove {
display: none; display: none;
position: absolute; position: absolute;
...@@ -122,4 +130,9 @@ async function handleNext() { ...@@ -122,4 +130,9 @@ async function handleNext() {
} }
} }
} }
.market-group-box-hd {
display: flex;
align-items: center;
justify-content: space-between;
}
</style> </style>
...@@ -47,13 +47,13 @@ const handleSubmit = async () => { ...@@ -47,13 +47,13 @@ const handleSubmit = async () => {
<el-input v-model="form.name" /> <el-input v-model="form.name" />
</el-form-item> </el-form-item>
<el-form-item label="群组加入规则" prop="join_rule"> <el-form-item label="群组加入规则" prop="join_rule">
<el-input type="textarea" v-model="form.join_rule" show-word-limit maxlength="100" :autosize="{ minRows: 3, maxRows: 6 }"></el-input> <el-input type="textarea" v-model="form.join_rule" show-word-limit maxlength="100" :rows="4"></el-input>
</el-form-item> </el-form-item>
<el-form-item label="群组移除规则" prop="remove_rule"> <el-form-item label="群组移除规则" prop="remove_rule">
<el-input type="textarea" v-model="form.remove_rule" show-word-limit maxlength="100" :autosize="{ minRows: 3, maxRows: 6 }"></el-input> <el-input type="textarea" v-model="form.remove_rule" show-word-limit maxlength="100" :rows="4"></el-input>
</el-form-item> </el-form-item>
<el-form-item label="设计群组原因" prop="reason"> <el-form-item label="设计群组原因" prop="reason">
<el-input type="textarea" v-model="form.reason" show-word-limit maxlength="100" :autosize="{ minRows: 3, maxRows: 6 }"></el-input> <el-input type="textarea" v-model="form.reason" show-word-limit maxlength="100" :rows="4"></el-input>
</el-form-item> </el-form-item>
</el-form> </el-form>
<template #footer> <template #footer>
......
...@@ -42,20 +42,33 @@ const objectiveDialogVisible = ref(false) ...@@ -42,20 +42,33 @@ const objectiveDialogVisible = ref(false)
<el-button type="primary" size="large" @click="objectiveDialogVisible = true">业务部门的营销目标</el-button> <el-button type="primary" size="large" @click="objectiveDialogVisible = true">业务部门的营销目标</el-button>
</el-space> </el-space>
</el-row> </el-row>
<Flow v-model:nodes="tripStore.nodes" v-model:edges="tripStore.edges" :nodes-draggable="!isCheck" :nodes-connectable="!isCheck" style="height: 60vh"></Flow> <div style="height: 60vh">
<Flow id="step-flow-1" v-model:nodes="tripStore.nodes" v-model:edges="tripStore.edges" :disabled="isCheck"></Flow>
</div>
<div class="market-step-footer"> <div class="market-step-footer">
<el-button @click="handleSubmit" :disabled="isCheck">保存</el-button> <el-button @click="handleSubmit" :disabled="isCheck">保存</el-button>
<el-button type="primary" @click="handleNext">下一步</el-button> <el-button type="primary" @click="handleNext">下一步</el-button>
</div> </div>
<el-dialog title="当前面临的问题与挑战" v-model="problemDialogVisible" width="600"> <el-dialog title="当前面临的问题与挑战" v-model="problemDialogVisible" width="600">
<ul> <ul class="problem-list">
<li v-for="item in objectiveStore.problems" :key="item.id">{{ item.content }}</li> <li v-for="(item, index) in objectiveStore.problems" :key="item.id">问题/挑战{{ index + 1 }}{{ item.content }}</li>
</ul> </ul>
</el-dialog> </el-dialog>
<el-dialog title="业务部门的营销目标" v-model="objectiveDialogVisible" width="600"> <el-dialog title="业务部门的营销目标" v-model="objectiveDialogVisible" width="600">
<ul> <ul class="problem-list">
<li v-for="item in objectiveStore.objectives" :key="item.id">{{ item.content }}</li> <li v-for="(item, index) in objectiveStore.objectives" :key="item.id">营销目标{{ index + 1 }}{{ item.content }}</li>
</ul> </ul>
</el-dialog> </el-dialog>
</div> </div>
</template> </template>
<style lang="scss" scoped>
.problem-list {
li {
line-height: 24px;
}
li + li {
margin-top: 20px;
}
}
</style>
<script>
export default {
inheritAttrs: false
}
</script>
<script setup> <script setup>
import { StepEdge, EdgeLabelRenderer, getSmoothStepPath, useVueFlow } from '@vue-flow/core' import { StepEdge, EdgeLabelRenderer, getSmoothStepPath, useVueFlow, useEdge, MarkerType } from '@vue-flow/core'
import { Plus, Close } from '@element-plus/icons-vue' import { Plus } from '@element-plus/icons-vue'
import { nanoid } from 'nanoid' import { nanoid } from 'nanoid'
const props = defineProps({ const props = defineProps({
...@@ -14,30 +20,50 @@ const props = defineProps({ ...@@ -14,30 +20,50 @@ const props = defineProps({
data: { type: Object, required: false }, data: { type: Object, required: false },
markerEnd: { type: String, required: false }, markerEnd: { type: String, required: false },
style: { type: Object, required: false }, style: { type: Object, required: false },
selected: { type: Boolean, required: false } selected: { type: Boolean, required: false },
disabled: { type: Boolean, required: false }
}) })
const path = computed(() => getSmoothStepPath(props)) const path = computed(() => getSmoothStepPath(props))
const { addNodes, removeEdges, nodesConnectable } = useVueFlow() const { addNodes, addEdges, removeEdges } = useVueFlow()
function onAdd() { const { edge } = useEdge()
const position = { x: 0, y: 0 } function addNodeBetweenEdges() {
const { source, target, sourceNode, targetNode } = edge
const newNodeId = nanoid(4)
const newNode = { const newNode = {
id: nanoid(4), id: newNodeId,
type: 'custom', type: 'custom',
label: `旅程节点`, label: `旅程节点`,
data: { label: '旅程节点' }, data: { label: '旅程节点' },
position position: {
x: (sourceNode.position.x + targetNode.position.x) / 2,
y: (sourceNode.position.y + targetNode.position.y) / 2
}
} }
addNodes([newNode]) addNodes([newNode])
}
</script>
<script> const newEdges = [
export default { {
inheritAttrs: false id: `${source}->${newNodeId}`,
type: 'custom',
source: source,
target: newNodeId,
animated: true,
markerEnd: MarkerType.ArrowClosed
},
{
id: `${newNodeId}->${target}`,
type: 'custom',
source: newNodeId,
target: target,
animated: true,
markerEnd: MarkerType.ArrowClosed
}
]
removeEdges([props.id])
addEdges(newEdges)
} }
</script> </script>
...@@ -50,10 +76,9 @@ export default { ...@@ -50,10 +76,9 @@ export default {
position: 'absolute', position: 'absolute',
transform: `translate(-50%, -50%) translate(${path[1]}px,${path[2]}px)` transform: `translate(-50%, -50%) translate(${path[1]}px,${path[2]}px)`
}" }"
v-if="nodesConnectable"> v-if="!disabled">
<el-button-group> <el-button-group>
<el-button :icon="Plus" circle @click="onAdd"></el-button> <el-button :icon="Plus" circle @click="addNodeBetweenEdges"></el-button>
<el-button :icon="Close" circle @click="removeEdges([id])"></el-button>
</el-button-group> </el-button-group>
</div> </div>
</EdgeLabelRenderer> </EdgeLabelRenderer>
......
...@@ -5,12 +5,20 @@ import NodeEnd from './NodeEnd.vue' ...@@ -5,12 +5,20 @@ import NodeEnd from './NodeEnd.vue'
import NodeCustom from './NodeCustom.vue' import NodeCustom from './NodeCustom.vue'
import EdgeCustom from './EdgeCustom.vue' import EdgeCustom from './EdgeCustom.vue'
import { nanoid } from 'nanoid' import { nanoid } from 'nanoid'
defineProps({ import { useLayout } from './useLayout'
process: { type: Number, default: 1 }
const props = defineProps({
id: {
type: String,
default() {
return nanoid()
}
},
process: { type: Number, default: 1 },
disabled: { type: Boolean, default: false }
}) })
const id = nanoid()
const { onConnect, addEdges } = useVueFlow(id)
const { onConnect, addEdges, fitView, nodes, edges, setNodes, findNode } = useVueFlow(props.id)
onConnect(params => { onConnect(params => {
addEdges([ addEdges([
{ {
...@@ -21,27 +29,35 @@ onConnect(params => { ...@@ -21,27 +29,35 @@ onConnect(params => {
} }
]) ])
}) })
const { layout } = useLayout(findNode)
async function layoutGraph(direction) {
setNodes(layout(nodes.value, edges.value, direction))
nextTick(() => {
fitView()
})
}
</script> </script>
<template> <template>
<VueFlow <VueFlow
:id="id" :id="id"
fit-view-on-init
:connection-radius="30" :connection-radius="30"
snap-to-grid :nodes-draggable="false"
:snap-grid="[180, 180]" :nodes-connectable="false"
:connection-line-options="{ markerEnd: MarkerType.ArrowClosed, type: ConnectionLineType.Straight }"> :connection-line-options="{ markerEnd: MarkerType.ArrowClosed, type: ConnectionLineType.Straight }"
@nodes-initialized="layoutGraph('LR')">
<template #node-start="props"> <template #node-start="props">
<NodeStart :process="process" v-bind="props" /> <NodeStart :process="process" :disabled="disabled" v-bind="props" />
</template> </template>
<template #node-end="props"> <template #node-end="props">
<NodeEnd :process="process" v-bind="props" /> <NodeEnd :process="process" :disabled="disabled" v-bind="props" />
</template> </template>
<template #node-custom="props"> <template #node-custom="props">
<NodeCustom :process="process" v-bind="props" /> <NodeCustom :process="process" :disabled="disabled" v-bind="props" />
</template> </template>
<template #edge-custom="props"> <template #edge-custom="props">
<EdgeCustom :process="process" v-bind="props" /> <EdgeCustom :process="process" :disabled="disabled" v-bind="props" />
</template> </template>
</VueFlow> </VueFlow>
</template> </template>
......
<script setup> <script setup>
import { Position, Handle, useVueFlow } from '@vue-flow/core' import { Position, Handle, useVueFlow, useNode, MarkerType } from '@vue-flow/core'
import { CircleCloseFilled } from '@element-plus/icons-vue' import { CircleCloseFilled } from '@element-plus/icons-vue'
import NodeCustomForm from './NodeCustomForm.vue' import NodeCustomForm from './NodeCustomForm.vue'
const Flow = defineAsyncComponent(() => import('./Flow.vue')) const Flow = defineAsyncComponent(() => import('./Flow.vue'))
const props = defineProps(['label', 'data', 'process', 'selected', 'id']) const props = defineProps(['label', 'data', 'process', 'selected', 'id', 'disabled'])
const dialogVisible = ref(false) const dialogVisible = ref(false)
...@@ -62,7 +62,7 @@ watch( ...@@ -62,7 +62,7 @@ watch(
{ immediate: true } { immediate: true }
) )
const { removeNodes, nodesDraggable, nodesConnectable } = useVueFlow() const { removeNodes, removeEdges, addEdges, edges: parentEdges } = useVueFlow()
const isCompleted = computed(() => { const isCompleted = computed(() => {
return !!props.data.label return !!props.data.label
...@@ -72,31 +72,47 @@ const handleSubmit = async () => { ...@@ -72,31 +72,47 @@ const handleSubmit = async () => {
Object.assign(props.data, { nodes: nodes.value, edges: edges.value }) Object.assign(props.data, { nodes: nodes.value, edges: edges.value })
flowDialogVisible.value = false flowDialogVisible.value = false
} }
const { node } = useNode()
function removeNodeBetweenEdges() {
// 获取边
const leftEdge = parentEdges.value.find(edge => edge.target === node.id)
const rightEdge = parentEdges.value.find(edge => edge.source === node.id)
// 删除节点
removeNodes([node.id])
// 删除边
removeEdges([leftEdge.id, rightEdge.id])
// 添加边
addEdges([
{
id: `${leftEdge.source}->${rightEdge.target}`,
type: 'custom',
source: leftEdge.source,
target: rightEdge.target,
animated: true,
markerEnd: MarkerType.ArrowClosed
}
])
}
</script> </script>
<template> <template>
<div class="flow-node flow-node-custom" :class="{ 'is-completed': isCompleted }"> <div class="flow-node flow-node-custom" :class="{ 'is-completed': isCompleted }">
<el-icon class="flow-node-custom__remove" @click="removeNodes([id])" v-if="selected && nodesDraggable"><CircleCloseFilled /></el-icon> <el-icon class="flow-node-custom__remove" @click="removeNodeBetweenEdges" v-if="selected && !disabled"><CircleCloseFilled /></el-icon>
<Handle type="target" :position="Position.Left" /> <Handle type="target" :position="Position.Left" />
<div class="flow-node-custom__inner"> <div class="flow-node-custom__inner">
<el-button type="primary" size="small" @click="dialogVisible = true" v-if="nodesDraggable">编辑</el-button> <el-button type="primary" size="small" @click="dialogVisible = true" v-if="!disabled">编辑</el-button>
<el-button type="primary" size="small" @click="flowDialogVisible = true" v-if="process != 2">子流程</el-button> <el-button type="primary" size="small" @click="flowDialogVisible = true" v-if="process != 2">子流程</el-button>
<div class="flow-node__label">{{ data.label || label }}</div> <div class="flow-node__label">{{ data.label || label }}</div>
</div> </div>
<Handle type="source" :position="Position.Right" /> <Handle type="source" :position="Position.Right" />
<NodeCustomForm :data="data" :process="process" v-model="dialogVisible" v-if="dialogVisible"></NodeCustomForm> <NodeCustomForm :id="id" :data="data" :process="process" v-model="dialogVisible" v-if="dialogVisible"></NodeCustomForm>
<el-dialog title="自动化营销旅程设计-二级流程" append-to-body width="1000" v-model="flowDialogVisible"> <el-dialog title="自动化营销旅程设计-二级流程" append-to-body width="1000" v-model="flowDialogVisible">
<Flow <Flow v-model:nodes="nodes" v-model:edges="edges" :process="2" :disabled="disabled" style="height: 500px"></Flow>
v-model:nodes="nodes"
v-model:edges="edges"
:process="2"
:nodes-draggable="nodesDraggable"
:nodes-connectable="nodesConnectable"
style="height: 500px"></Flow>
<template #footer> <template #footer>
<el-row justify="center"> <el-row justify="center">
<el-button plain auto-insert-space @click="flowDialogVisible = false">关闭</el-button> <el-button plain auto-insert-space @click="flowDialogVisible = false">关闭</el-button>
<el-button type="primary" auto-insert-space @click="handleSubmit" :disabled="!nodesDraggable">保存</el-button> <el-button type="primary" auto-insert-space @click="handleSubmit" :disabled="disabled">保存</el-button>
</el-row> </el-row>
</template> </template>
</el-dialog> </el-dialog>
......
<script setup> <script setup>
import { useVueFlow } from '@vue-flow/core'
import { useMapStore } from '@/stores/map' import { useMapStore } from '@/stores/map'
const materialTypeList = useMapStore().getMapValuesByKey('experiment_marketing_material_type') const materialTypeList = useMapStore().getMapValuesByKey('experiment_marketing_material_type')
const emit = defineEmits(['update:modelValue']) const emit = defineEmits(['update:modelValue'])
const props = defineProps(['data', 'process']) const props = defineProps(['id', 'data', 'process'])
const title = computed(() => { const title = computed(() => {
const subTitle = props.process == 2 ? '二级流程节点' : '一级流程节点' const subTitle = props.process == 2 ? '二级流程节点' : '一级流程节点'
...@@ -26,9 +27,12 @@ const rules = reactive({ ...@@ -26,9 +27,12 @@ const rules = reactive({
label: [{ required: true, message: '请输入', trigger: 'blur' }] label: [{ required: true, message: '请输入', trigger: 'blur' }]
}) })
const { updateNode } = useVueFlow()
const handleSubmit = async () => { const handleSubmit = async () => {
await formRef.value?.validate() await formRef.value?.validate()
Object.assign(props.data, { ...form }) updateNode(props.id, { label: form.label, data: form })
// Object.assign(props.data, { ...form })
emit('update:modelValue', false) emit('update:modelValue', false)
} }
</script> </script>
...@@ -40,7 +44,7 @@ const handleSubmit = async () => { ...@@ -40,7 +44,7 @@ const handleSubmit = async () => {
<el-input v-model="form.label"></el-input> <el-input v-model="form.label"></el-input>
</el-form-item> </el-form-item>
<el-form-item label="流程节点说明" prop="desc"> <el-form-item label="流程节点说明" prop="desc">
<el-input type="textarea" v-model="form.desc" show-word-limit maxlength="200" :autosize="{ minRows: 3, maxRows: 6 }"></el-input> <el-input type="textarea" v-model="form.desc" show-word-limit maxlength="200" :rows="3"></el-input>
</el-form-item> </el-form-item>
<template v-if="process == 2"> <template v-if="process == 2">
<el-form-item label="是否用到营销物料" prop="use_material"> <el-form-item label="是否用到营销物料" prop="use_material">
......
<script setup> <script setup>
import { Position, Handle, useVueFlow } from '@vue-flow/core' import { Position, Handle } from '@vue-flow/core'
defineProps(['label', 'process']) defineProps(['label', 'process', 'disabled'])
const { nodesDraggable } = useVueFlow()
</script> </script>
<template> <template>
<div class="flow-node flow-node-end"> <div class="flow-node flow-node-end">
<div class="flow-node__label">{{ label }}</div> <div class="flow-node__label">{{ label }}</div>
<Handle type="target" :position="Position.Left" /> <Handle type="target" :position="Position.Left" />
<p class="flow-node-tips" v-if="process == 1 && nodesDraggable">自动化旅程的结束不需要维护</p> <p class="flow-node-tips" v-if="process == 1 && !disabled">自动化旅程的结束不需要维护</p>
</div> </div>
</template> </template>
......
<script setup> <script setup>
import { Position, Handle, useVueFlow } from '@vue-flow/core' import { Position, Handle } from '@vue-flow/core'
const props = defineProps(['label', 'data', 'process']) const props = defineProps(['label', 'data', 'process', 'disabled'])
const { nodesDraggable } = useVueFlow()
const dialogVisible = ref(false) const dialogVisible = ref(false)
...@@ -38,7 +36,7 @@ const handleSubmit = async () => { ...@@ -38,7 +36,7 @@ const handleSubmit = async () => {
} }
const handleClick = () => { const handleClick = () => {
if (!nodesDraggable.value) return if (props.disabled) return
dialogVisible.value = true dialogVisible.value = true
} }
</script> </script>
...@@ -47,7 +45,7 @@ const handleClick = () => { ...@@ -47,7 +45,7 @@ const handleClick = () => {
<div class="flow-node flow-node-start" @click="handleClick"> <div class="flow-node flow-node-start" @click="handleClick">
<div class="flow-node__label">{{ label }}</div> <div class="flow-node__label">{{ label }}</div>
<Handle type="source" :position="Position.Right" /> <Handle type="source" :position="Position.Right" />
<p class="flow-node-tips" v-if="process == 1 && nodesDraggable">点击节点维护自动化营销旅程的触发条件</p> <p class="flow-node-tips" v-if="process == 1 && !disabled">点击节点维护自动化营销旅程的触发条件</p>
<el-dialog v-model="dialogVisible" title="自动化营销旅程设计-旅程触发" append-to-body width="600"> <el-dialog v-model="dialogVisible" title="自动化营销旅程设计-旅程触发" append-to-body width="600">
<el-form label-suffix=":" label-width="140" :model="form" :rules="rules" ref="formRef"> <el-form label-suffix=":" label-width="140" :model="form" :rules="rules" ref="formRef">
...@@ -62,7 +60,7 @@ const handleClick = () => { ...@@ -62,7 +60,7 @@ const handleClick = () => {
</el-radio-group> </el-radio-group>
</el-form-item> </el-form-item>
<el-form-item label="旅程触发条件说明" prop="desc"> <el-form-item label="旅程触发条件说明" prop="desc">
<el-input type="textarea" v-model="form.desc" show-word-limit maxlength="200" :autosize="{ minRows: 3, maxRows: 6 }"></el-input> <el-input type="textarea" v-model="form.desc" show-word-limit maxlength="200" :rows="3"></el-input>
</el-form-item> </el-form-item>
</el-form> </el-form>
<template #footer> <template #footer>
......
import dagre from '@dagrejs/dagre'
import { Position } from '@vue-flow/core'
import { ref } from 'vue'
/**
* Composable to run the layout algorithm on the graph.
* It uses the `dagre` library to calculate the layout of the nodes and edges.
*/
export function useLayout(findNode) {
const graph = ref(new dagre.graphlib.Graph())
const previousDirection = ref('LR')
function layout(nodes, edges, direction) {
nodes = sortNodes(nodes, edges)
// we create a new graph instance, in case some nodes/edges were removed, otherwise dagre would act as if they were still there
const dagreGraph = new dagre.graphlib.Graph()
graph.value = dagreGraph
dagreGraph.setDefaultEdgeLabel(() => ({}))
const isHorizontal = direction === 'LR'
dagreGraph.setGraph({ rankdir: direction, ranksep: 160 })
previousDirection.value = direction
for (const node of nodes) {
// if you need width+height of nodes for your layout, you can use the dimensions property of the internal node (`GraphNode` type)
const graphNode = findNode(node.id)
dagreGraph.setNode(node.id, { width: graphNode.dimensions.width || 182, height: graphNode.dimensions.height || 162 })
}
for (const edge of edges) {
dagreGraph.setEdge(edge.source, edge.target)
}
dagre.layout(dagreGraph)
// set nodes with updated positions
return nodes.map(node => {
const nodeWithPosition = dagreGraph.node(node.id)
return {
...node,
targetPosition: isHorizontal ? Position.Left : Position.Top,
sourcePosition: isHorizontal ? Position.Right : Position.Bottom,
position: { x: nodeWithPosition.x, y: nodeWithPosition.y }
}
})
function sortNodes(nodes, edges) {
const nodesMap = new Map()
nodes.forEach(node => {
nodesMap.set(node.id, node)
})
// Perform topological sort to determine the order of nodes
const sortedNodes = []
const visited = new Set()
const visit = nodeId => {
if (!visited.has(nodeId)) {
visited.add(nodeId)
edges.filter(edge => edge.source === nodeId).forEach(edge => visit(edge.target))
sortedNodes.push(nodesMap.get(nodeId))
}
}
nodesMap.forEach((_, nodeId) => visit(nodeId))
return sortedNodes.reverse()
}
}
return { graph, layout, previousDirection }
}
...@@ -39,7 +39,7 @@ export const useMemberStore = defineStore('member', { ...@@ -39,7 +39,7 @@ export const useMemberStore = defineStore('member', {
this.setMember(data) this.setMember(data)
}, },
setMember(data: MemberState) { setMember(data: MemberState) {
this.member = data this.member = Object.assign(this.member, data)
}, },
addAttr(data: Omit<MemberAttr, 'id'>) { addAttr(data: Omit<MemberAttr, 'id'>) {
this.member.attrs.push({ id: nanoid(4), ...data }) this.member.attrs.push({ id: nanoid(4), ...data })
......
...@@ -61,28 +61,28 @@ async function handleNext(data, isCheck = false) { ...@@ -61,28 +61,28 @@ async function handleNext(data, isCheck = false) {
<el-divider /> <el-divider />
<el-tabs v-model="activeTab" stretch class="market-tabs"> <el-tabs v-model="activeTab" stretch class="market-tabs">
<el-tab-pane lazy label="第1步" :name="1"> <el-tab-pane lazy label="第1步" :name="1">
<Step1 :data="detail.step1" @submit="handleSubmit" @next="handleNext"></Step1> <Step1 @submit="handleSubmit" @next="handleNext"></Step1>
</el-tab-pane> </el-tab-pane>
<el-tab-pane lazy label="第2步" :name="2"> <el-tab-pane lazy label="第2步" :name="2">
<Step2 :data="detail.step2" @submit="handleSubmit" @next="handleNext"></Step2> <Step2 @submit="handleSubmit" @next="handleNext"></Step2>
</el-tab-pane> </el-tab-pane>
<el-tab-pane lazy label="第3步" :name="3"> <el-tab-pane lazy label="第3步" :name="3">
<Step3 :data="detail.step3" @submit="handleSubmit" @next="handleNext"></Step3> <Step3 @submit="handleSubmit" @next="handleNext"></Step3>
</el-tab-pane> </el-tab-pane>
<el-tab-pane lazy label="第4步" :name="4"> <el-tab-pane lazy label="第4步" :name="4">
<Step4 :data="detail.step4" @submit="handleSubmit" @next="handleNext"></Step4> <Step4 @submit="handleSubmit" @next="handleNext"></Step4>
</el-tab-pane> </el-tab-pane>
<el-tab-pane lazy label="第5步" :name="5"> <el-tab-pane lazy label="第5步" :name="5">
<Step5 :data="detail.step5" @submit="handleSubmit" @next="handleNext"></Step5> <Step5 @submit="handleSubmit" @next="handleNext"></Step5>
</el-tab-pane> </el-tab-pane>
<el-tab-pane lazy label="第6步" :name="6"> <el-tab-pane lazy label="第6步" :name="6">
<Step6 :data="detail.step6" @submit="handleSubmit" @next="handleNext"></Step6> <Step6 @submit="handleSubmit" @next="handleNext"></Step6>
</el-tab-pane> </el-tab-pane>
<el-tab-pane lazy label="第7步" :name="7"> <el-tab-pane lazy label="第7步" :name="7">
<Step7 :data="detail.step7" :detail="detail" @submit="handleSubmit" @next="handleNext"></Step7> <Step7 @submit="handleSubmit" @next="handleNext"></Step7>
</el-tab-pane> </el-tab-pane>
<el-tab-pane lazy label="第8步" :name="8"> <el-tab-pane lazy label="第8步" :name="8">
<Step8 :data="detail.step8" @submit="handleSubmit" @next="handleNext"></Step8> <Step8 @submit="handleSubmit" @next="handleNext"></Step8>
</el-tab-pane> </el-tab-pane>
</el-tabs> </el-tabs>
</AppCard> </AppCard>
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论