提交 2045c603 authored 作者: lhh's avatar lhh

营销报告

上级 45efe60f
......@@ -150,7 +150,12 @@ defineExpose({ generateImage, generatePdf })
<li>指导教师:{{ teacherName }}</li>
</ul>
</div>
<section id="step1" class="section" :class="{ hide: step !== 1 }" v-element-visibility="state => onElementVisibility(state, 1)">
<section
id="step1"
class="section"
:class="{ hide: step !== 1 }"
v-element-visibility="state => onElementVisibility(state, 1)"
>
<h2>一、营销背景</h2>
<h3>(一)当前业务面临的问题及挑战</h3>
<template v-for="(item, index) in objectiveStore.problems" :key="item.id">
......@@ -161,7 +166,12 @@ defineExpose({ generateImage, generatePdf })
<p>营销目标{{ index + 1 }}{{ item.content }}</p>
</template>
</section>
<section id="step2" class="section" :class="{ hide: step !== 2 }" v-element-visibility="state => onElementVisibility(state, 2)">
<section
id="step2"
class="section"
:class="{ hide: step !== 2 }"
v-element-visibility="state => onElementVisibility(state, 2)"
>
<h2>二、营销渠道</h2>
<p>本次营销选择的主要渠道为:</p>
<template v-for="(item, index) in connectionStore.activeConnections" :key="item.id">
......@@ -170,7 +180,12 @@ defineExpose({ generateImage, generatePdf })
<p>选择该渠道的原因为:{{ item.content }}</p>
</template>
</section>
<section id="step3" class="section" :class="{ hide: step !== 3 }" v-element-visibility="state => onElementVisibility(state, 3)">
<section
id="step3"
class="section"
:class="{ hide: step !== 3 }"
v-element-visibility="state => onElementVisibility(state, 3)"
>
<h2>三、用户分析</h2>
<h3>(一)用户性别分析</h3>
<p>{{ memberStore.member.sex }}</p>
......@@ -183,7 +198,12 @@ defineExpose({ generateImage, generatePdf })
<img :src="memberStore.member.source_file" />
</div>
</section>
<section id="step4" class="section" :class="{ hide: step !== 4 }" v-element-visibility="state => onElementVisibility(state, 4)">
<section
id="step4"
class="section"
:class="{ hide: step !== 4 }"
v-element-visibility="state => onElementVisibility(state, 4)"
>
<h2>四、用户标签体系设计</h2>
<template v-for="(item, index) in labelStore.treeLabels" :key="item.id">
<h3>{{ numberToChinese(index + 1) }}{{ item.name }}</h3>
......@@ -195,7 +215,12 @@ defineExpose({ generateImage, generatePdf })
</template>
</template>
</section>
<section id="step5" class="section" :class="{ hide: step !== 5 }" v-element-visibility="state => onElementVisibility(state, 5)">
<section
id="step5"
class="section"
:class="{ hide: step !== 5 }"
v-element-visibility="state => onElementVisibility(state, 5)"
>
<h2>五、用户精准分群设计</h2>
<h3>(一)静态群组</h3>
<p>本项目设计如下静态群组:</p>
......@@ -214,11 +239,22 @@ defineExpose({ generateImage, generatePdf })
<p>设计该群组的原因是:{{ item.reason }}</p>
</template>
</section>
<section id="step6" class="section" :class="{ hide: step !== 6 }" v-element-visibility="state => onElementVisibility(state, 6)">
<section
id="step6"
class="section"
:class="{ hide: step !== 6 }"
v-element-visibility="state => onElementVisibility(state, 6)"
>
<h2>六、自动化营销旅程设计</h2>
<h3>(一)一级流程</h3>
<p>本项目设计一级流程图如下。</p>
<Flow :nodes="tripStore.nodes" :edges="tripStore.edges" :nodes-draggable="false" :nodes-connectable="false" style="height: 200px"></Flow>
<Flow
:nodes="tripStore.nodes"
:edges="tripStore.edges"
:nodes-draggable="false"
:nodes-connectable="false"
style="height: 200px"
></Flow>
<p>相关节点设计说明如下:</p>
<template v-for="(item, index) in tripStore.nodes" :key="item.id">
<p>{{ index + 1 }}{{ item.data.label || item.label }}节点</p>
......@@ -229,13 +265,22 @@ defineExpose({ generateImage, generatePdf })
<template v-else-if="item.type === 'end'">结束节点</template>
<template v-else>业务节点</template>
</p>
<p v-if="item.type === 'start'">节点配置:触发时机为“{{ item.data.time }}”,触发条件为“{{ item.data.condition }}”。</p>
<p v-if="item.type === 'start'">
节点配置:触发时机为“{{ item.data.time }}”,触发条件为“{{ item.data.condition }}”。
</p>
</template>
<h3>(二)二级流程</h3>
<p>本项目如下一级流程节点设计了二级流程。</p>
<template v-for="(item, index) in tripStore.node1List" :key="item.id">
<p>{{ index + 1 }}{{ item.data.label || item.label }}节点。该节点设计的二级流程图如下:</p>
<Flow :process="2" :nodes="item.data.nodes" :edges="item.data.edges" :nodes-draggable="false" :nodes-connectable="false" style="height: 200px"></Flow>
<Flow
:process="2"
:nodes="item.data.nodes"
:edges="item.data.edges"
:nodes-draggable="false"
:nodes-connectable="false"
style="height: 200px"
></Flow>
<p>该二级流程图节点说明如下:</p>
<template v-for="(item, index) in item.data.nodes" :key="item.id">
<p>{{ index + 1 }}{{ item.data.label || item.label }}节点</p>
......@@ -246,19 +291,33 @@ defineExpose({ generateImage, generatePdf })
<template v-else-if="item.type === 'end'">结束节点</template>
<template v-else>业务节点</template>
</p>
<p v-if="item.type === 'start'">节点配置:触发时机为“{{ item.data.time }}”,触发条件为“{{ item.data.condition }}”。</p>
<p v-if="item.type === 'start'">
节点配置:触发时机为“{{ item.data.time }}”,触发条件为“{{ item.data.condition }}”。
</p>
<p v-if="item.type === 'custom'">是否用到营销物料:{{ item.use_material }}</p>
<p v-if="item.type === 'custom'">营销物料类型:{{ item.material_type }}</p>
</template>
</template>
</section>
<section id="step7" class="section" :class="{ hide: step !== 7 }" v-element-visibility="state => onElementVisibility(state, 7)">
<section
id="step7"
class="section"
:class="{ hide: step !== 7 }"
v-element-visibility="state => onElementVisibility(state, 7)"
>
<h2>七、营销物料设计</h2>
<p>本项目设计如下营销物料。</p>
<AppList v-bind="listOptions"></AppList>
</section>
<ul class="market-report-step">
<li v-for="(item, index) in steps" :key="index" :class="{ 'is-active': index + 1 === step }" @click="handleClick(item)">{{ item.name }}</li>
<li
v-for="(item, index) in steps"
:key="index"
:class="{ 'is-active': index + 1 === step }"
@click="handleClick(item)"
>
{{ item.name }}
</li>
</ul>
</div>
</div>
......
......@@ -11,7 +11,7 @@ export function getSearchCriteria() {
}
// 获取列表
export function getRecordList(params?: { name: string, sno_number: string }) {
export function getRecordList(params?: { name?: string, sno_number?: any }) {
return httpRequest.get('/api/lab/v1/experiment/marketing-planning/record-list', { params })
}
......
<script setup>
import { toBlob, toCanvas } from 'html-to-image'
import { jsPDF } from 'jspdf'
// import { saveAs } from 'file-saver'
import { upload } from '@/utils/upload'
import Flow from './flow/Flow.vue'
import { useMarketStore } from '../stores/market'
import { vElementVisibility } from '@vueuse/components'
import scrollIntoView from 'scroll-into-view-if-needed'
const props = defineProps({
step: { type: Number, default: 1 },
experimentName: { type: String },
studentName: { type: String },
teacherName: { type: String },
detail: { type: Object }
})
const marketStore = useMarketStore()
const { objectiveStore, connectionStore, memberStore, labelStore, groupStore, tripStore, materialStore } = marketStore
watch(
() => props.detail,
() => {
if (props.detail) marketStore.setData(props.detail)
},
{ immediate: true }
)
const step = ref(props.step)
const steps = [
{ name: '营销背景', step: 1 },
{ name: '营销渠道', step: 2 },
{ name: '用户分析', step: 3 },
{ name: '用户标签', step: 4 },
{ name: '用户分群', step: 5 },
{ name: '自动化旅程', step: 6 },
{ name: '营销物料', step: 7 }
]
const listOptions = computed(() => {
return {
columns: [
{ label: '序号', type: 'index', width: 60 },
{ label: '一级流程节点', prop: 'node1' },
{ label: '二级流程节点', prop: 'node2' },
{ label: '营销物料类型', prop: 'type' },
{ label: '物料风格', prop: 'style' },
{ label: '物料侧重点', prop: 'desc' },
{ label: '物料更新频率', prop: 'update_rule' }
],
data: materialStore.materials
}
})
const reportRef = ref()
// 生成图片
async function generateImage() {
const blob = await toBlob(reportRef.value, { width: 1000 })
const url = await upload(blob)
return url
}
// 生成PDF
async function generatePdf() {
// const blob = await toBlob(reportRef.value, { width: 1000 })
// saveAs(blob, '营销策划报告.png')
const canvas = await toCanvas(reportRef.value)
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 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')
}
function onElementVisibility(state, stepIndex) {
if (state) step.value = stepIndex
}
function handleClick(item) {
const node = document.getElementById(`step${item.step}`)
scrollIntoView(node, {
behavior: 'smooth',
block: 'start'
})
step.value = item.step
}
function numberToChinese(num) {
// 简单的中文数字映射
const chineseNums = ['零', '一', '二', '三', '四', '五', '六', '七', '八', '九']
const chineseUnits = ['', '十', '百', '千', '万', '十', '百', '千', '亿']
if (num === 0) return chineseNums[0]
let str = ''
let unitIndex = 0
while (num > 0) {
let digit = num % 10 // 取出个位数字
if (digit !== 0) {
// 如果不为零,则添加中文数字和单位
str = chineseNums[digit] + chineseUnits[unitIndex] + str
} else if (str.length > 0 && str[0] !== chineseNums[0]) {
// 如果当前数字为零且上一个数字不为零,添加零
str = chineseNums[0] + str
}
num = Math.floor(num / 10)
unitIndex++
// 当到万位时,重置单位索引(因为接下来是亿)
if (unitIndex === 5) {
unitIndex = 1
}
}
// 去除开头的零(如果有的话)
if (str.startsWith(chineseNums[0])) {
str = str.slice(1)
}
return str
}
defineExpose({ generateImage, generatePdf })
</script>
<template>
<div class="market-report-wrapper">
<div class="market-report" ref="reportRef">
<div class="market-report-header">
<h1>{{ experimentName }}”实验<br />营销策划报告</h1>
<ul>
<li>策划人:{{ studentName }}</li>
<li>指导教师:{{ teacherName }}</li>
</ul>
</div>
<section id="step1" class="section" :class="{ hide: step !== 1 }" v-element-visibility="state => onElementVisibility(state, 1)">
<h2>一、营销背景</h2>
<h3>(一)当前业务面临的问题及挑战</h3>
<template v-for="(item, index) in objectiveStore.problems" :key="item.id">
<p>问题与挑战{{ index + 1 }}{{ item.content }}</p>
</template>
<h3>(二)业务部门营销目标</h3>
<template v-for="(item, index) in objectiveStore.objectives" :key="item.id">
<p>营销目标{{ index + 1 }}{{ item.content }}</p>
</template>
</section>
<section id="step2" class="section" :class="{ hide: step !== 2 }" v-element-visibility="state => onElementVisibility(state, 2)">
<h2>二、营销渠道</h2>
<p>本次营销选择的主要渠道为:</p>
<template v-for="(item, index) in connectionStore.activeConnections" :key="item.id">
<h3>{{ numberToChinese(index + 1) }}{{ item.type_name }}</h3>
<p>当前渠道拥有的用户数为:{{ item.member_count }}人,拥有的用户事件数量为:{{ item.event_count }}</p>
<p>选择该渠道的原因为:{{ item.content }}</p>
</template>
</section>
<section id="step3" class="section" :class="{ hide: step !== 3 }" v-element-visibility="state => onElementVisibility(state, 3)">
<h2>三、用户分析</h2>
<h3>(一)用户性别分析</h3>
<p>{{ memberStore.member.sex }}</p>
<div style="text-align: center">
<img :src="memberStore.member.sex_file" />
</div>
<h3>(二)用户数据来源分析</h3>
<p>{{ memberStore.member.source }}</p>
<div style="text-align: center">
<img :src="memberStore.member.source_file" />
</div>
</section>
<section id="step4" class="section" :class="{ hide: step !== 4 }" v-element-visibility="state => onElementVisibility(state, 4)">
<h2>四、用户标签体系设计</h2>
<template v-for="(item, index) in labelStore.treeLabels" :key="item.id">
<h3>{{ numberToChinese(index + 1) }}{{ item.name }}</h3>
<p>本项目设计如下{{ item.name }}</p>
<template v-for="(label, index) in item.children" :key="label.id">
<p>{{ index + 1 }}{{ label.name }}</p>
<p>该标签关联“{{ label.data_type }}”,关联字段为:{{ label.attr_name || label.event_name }}</p>
<p>该标签的设置规则为:{{ label.desc }}</p>
</template>
</template>
</section>
<section id="step5" class="section" :class="{ hide: step !== 5 }" v-element-visibility="state => onElementVisibility(state, 5)">
<h2>五、用户精准分群设计</h2>
<h3>(一)静态群组</h3>
<p>本项目设计如下静态群组:</p>
<template v-for="(item, index) in groupStore.staticGroups" :key="item.id">
<h3>{{ index + 1 }}{{ item.name }}</h3>
<p>该群组的加入规则为:{{ item.join_rule }}</p>
<p>该群组的移除规则为:{{ item.remove_rule }}</p>
<p>设计该群组的原因是:{{ item.reason }}</p>
</template>
<h3>(二)动态群组</h3>
<p>本项目设计如下动态群组:</p>
<template v-for="(item, index) in groupStore.dynamicGroups" :key="item.id">
<h3>{{ index + 1 }}{{ item.name }}</h3>
<p>该群组的加入规则为:{{ item.join_rule }}</p>
<p>该群组的移除规则为:{{ item.remove_rule }}</p>
<p>设计该群组的原因是:{{ item.reason }}</p>
</template>
</section>
<section id="step6" class="section" :class="{ hide: step !== 6 }" v-element-visibility="state => onElementVisibility(state, 6)">
<h2>六、自动化营销旅程设计</h2>
<h3>(一)一级流程</h3>
<p>本项目设计一级流程图如下。</p>
<Flow :nodes="tripStore.nodes" :edges="tripStore.edges" :nodes-draggable="false" :nodes-connectable="false" style="height: 200px"></Flow>
<p>相关节点设计说明如下:</p>
<template v-for="(item, index) in tripStore.nodes" :key="item.id">
<p>{{ index + 1 }}{{ item.data.label || item.label }}节点</p>
<p v-if="item.data.desc">节点说明:{{ item.data.desc }}</p>
<p>
节点类型:
<template v-if="item.type === 'start'">开始节点</template>
<template v-else-if="item.type === 'end'">结束节点</template>
<template v-else>业务节点</template>
</p>
<p v-if="item.type === 'start'">节点配置:触发时机为“{{ item.data.time }}”,触发条件为“{{ item.data.condition }}”。</p>
</template>
<h3>(二)二级流程</h3>
<p>本项目如下一级流程节点设计了二级流程。</p>
<template v-for="(item, index) in tripStore.node1List" :key="item.id">
<p>{{ index + 1 }}{{ item.data.label || item.label }}节点。该节点设计的二级流程图如下:</p>
<Flow :process="2" :nodes="item.data.nodes" :edges="item.data.edges" :nodes-draggable="false" :nodes-connectable="false" style="height: 200px"></Flow>
<p>该二级流程图节点说明如下:</p>
<template v-for="(item, index) in item.data.nodes" :key="item.id">
<p>{{ index + 1 }}{{ item.data.label || item.label }}节点</p>
<p v-if="item.data.desc">节点说明:{{ item.data.desc }}</p>
<p>
节点类型:
<template v-if="item.type === 'start'">开始节点</template>
<template v-else-if="item.type === 'end'">结束节点</template>
<template v-else>业务节点</template>
</p>
<p v-if="item.type === 'start'">节点配置:触发时机为“{{ item.data.time }}”,触发条件为“{{ item.data.condition }}”。</p>
<p v-if="item.type === 'custom'">是否用到营销物料:{{ item.use_material }}</p>
<p v-if="item.type === 'custom'">营销物料类型:{{ item.material_type }}</p>
</template>
</template>
</section>
<section id="step7" class="section" :class="{ hide: step !== 7 }" v-element-visibility="state => onElementVisibility(state, 7)">
<h2>七、营销物料设计</h2>
<p>本项目设计如下营销物料。</p>
<AppList v-bind="listOptions"></AppList>
</section>
<ul class="market-report-step">
<li v-for="(item, index) in steps" :key="index" :class="{ 'is-active': index + 1 === step }" @click="handleClick(item)">{{ item.name }}</li>
</ul>
</div>
</div>
</template>
<style lang="scss" scoped>
.market-report-wrapper {
max-width: 1000px;
margin: 0 auto;
}
.market-report {
padding: 0 40px;
background-color: #fff;
position: relative;
.market-report-header {
padding: 40px 0;
margin-bottom: 40px;
border-bottom: 1px solid #eee;
text-align: center;
h1 {
color: rgba(16, 16, 16, 1);
font-size: 18px;
font-weight: 700;
line-height: 25px;
}
ul {
margin-top: 40px;
display: flex;
align-items: center;
justify-content: space-evenly;
}
li {
color: rgba(118, 117, 117, 1);
font-weight: 400;
letter-spacing: 1px;
}
}
section {
margin-bottom: 20px;
// &.hide {
// display: none;
// }
}
h2 {
padding: 10px 0;
font-size: 20px;
}
h3 {
margin-top: 10px;
padding: 10px 0;
font-size: 16px;
}
p {
margin-left: 20px;
font-size: 14px;
line-height: 24px;
strong {
font-weight: bold;
}
}
img {
max-width: 100%;
}
.market-report-step {
position: fixed;
right: 50px;
top: 50%;
transform: translateY(-50%);
li {
margin-top: 10px;
width: 80px;
height: 58px;
line-height: 58px;
text-align: center;
border-radius: 10px;
border: 1px solid rgb(187, 187, 187);
box-sizing: border-box;
cursor: pointer;
background-color: #fff;
&.is-active {
background-color: rgb(189, 248, 180);
border: none;
}
}
}
}
</style>
......@@ -7,7 +7,8 @@ const routes: RouteRecordRaw[] = [
component: Layout,
children: [
{ path: '', component: () => import('./views/Index.vue') },
{ path: 'score', component: () => import('./views/Score.vue') }
{ path: 'score', component: () => import('./views/Score.vue') },
{ path: 'view', component: () => import('./views/View.vue') }
]
}
]
......
<script setup lang="ts">
import type { ExperimentInfo } from '../types'
import { getSearchCriteria, getRecordList } from '../api'
import Report from '@/modules/market/my/components/Report.vue'
const route = useRoute()
......@@ -79,9 +80,7 @@ const listOptions = computed(() => {
function isComplete(is: Boolean) {
let n = ''
is
? (n = '<div style="color: #009b3b; font-size:20px;">✓</div>')
: (n = '<div style="font-size:20px;">-</div>')
is ? (n = '<div style="color: #009b3b; font-size:20px;">✓</div>') : (n = '<div style="font-size:20px;">-</div>')
return n
}
</script>
......@@ -95,10 +94,27 @@ function isComplete(is: Boolean) {
<el-form-item label="实验学时">{{ experimentInfo?.length }}</el-form-item>
</el-form>
<el-divider />
<!-- <Report
:experimentName="11"
:teacherName="11"
:studentName="11"
ref="reportRef" /> -->
<h2 class="h2-title">营销策划</h2>
<AppList v-bind="listOptions">
<template #table-x="{ row }">
<el-button text type="primary">查看营销策划报告</el-button>
<!-- <el-button text type="primary">查看营销策划报告</el-button> -->
<router-link
target="_blank"
:to="{
path: '/market/review/view',
query: {
id: row.id,
snoNumber: row.sno_number,
experiment_id: route.query.experiment_id
}
}"
><el-button text type="primary">查看营销策划报告</el-button></router-link
>
<router-link
target="_blank"
:to="{
......@@ -113,7 +129,6 @@ function isComplete(is: Boolean) {
}"
><el-button text type="primary">评分</el-button></router-link
>
<!-- <el-button text type="primary" @click="">评分</el-button> -->
</template>
</AppList>
</AppCard>
......
<script setup lang="ts">
import type { ExperimentInfo } from '../types'
import { getSearchCriteria, getRecordList } from '../api'
import Report from '@/modules/market/my/components/Report.vue'
const route = useRoute()
let experimentInfo = $ref<ExperimentInfo>()
getSearchCriteria().then((res: { data: { experiment: ExperimentInfo } }) => {
if (res?.data) {
const data = res.data.experiment
data.teacher_name = data.teacher.reduce((a: any, b: any) => a.push(b.name) && a, []).join(',')
experimentInfo = data
}
})
let detail = ref<any>({})
getRecordList({ sno_number: route.query.snoNumber }).then(res => {
if (res.data) {
detail.value = res.data.list[0]
}
})
const data = $computed(() => {
return detail.value?.details ? JSON.parse(detail.value.details) : {}
})
</script>
<template>
<AppCard>
<el-form label-suffix=":" inline class="info">
<el-form-item label="实验名称">{{ experimentInfo?.name }}</el-form-item>
<el-form-item label="课程名称">{{ experimentInfo?.course_name }}</el-form-item>
<el-form-item label="指导教师">{{ experimentInfo?.teacher_name }}</el-form-item>
<el-form-item label="实验学时">{{ experimentInfo?.length }}</el-form-item>
</el-form>
<el-divider />
<Report
:detail="data"
:experimentName="experimentInfo?.name"
:teacherName="experimentInfo?.teacher_name"
:studentName="detail?.name"
ref="reportRef"
/>
</AppCard>
</template>
<style lang="scss" scoped>
.info {
display: flex;
justify-content: space-between;
}
</style>
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论