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

Merge branch 'pro' into gdrtvu

...@@ -11,5 +11,6 @@ ...@@ -11,5 +11,6 @@
<div id="app"></div> <div id="app"></div>
<script type="module" src="/src/main.ts"></script> <script type="module" src="/src/main.ts"></script>
<script src="https://webapp-pub.ezijing.com/plugins/sky-agents/sky-agent.umd.cjs?v=1"></script> <script src="https://webapp-pub.ezijing.com/plugins/sky-agents/sky-agent.umd.cjs?v=1"></script>
<script src="https://webapp-pub.ezijing.com/plugins/tinymce/tinymce.min.js"></script>
</body> </body>
</html> </html>
差异被折叠。
...@@ -16,11 +16,13 @@ ...@@ -16,11 +16,13 @@
}, },
"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",
"@vue-flow/controls": "^1.0.4", "@vue-flow/controls": "^1.1.2",
"@vue-flow/core": "^1.17.4", "@vue-flow/core": "^1.39.0",
"@vueuse/components": "^10.11.0",
"@vueuse/core": "^10.9.0", "@vueuse/core": "^10.9.0",
"axios": "^1.6.8", "axios": "^1.6.8",
"blueimp-md5": "^2.19.0", "blueimp-md5": "^2.19.0",
...@@ -28,9 +30,13 @@ ...@@ -28,9 +30,13 @@
"echarts": "^5.5.0", "echarts": "^5.5.0",
"echarts-wordcloud": "^2.1.0", "echarts-wordcloud": "^2.1.0",
"element-plus": "^2.6.3", "element-plus": "^2.6.3",
"file-saver": "^2.0.5",
"html-to-image": "^1.11.11",
"jspdf": "^2.5.1",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"nanoid": "^5.0.7", "nanoid": "^5.0.7",
"pinia": "^2.1.7", "pinia": "^2.1.7",
"scroll-into-view-if-needed": "^3.1.0",
"vue": "^3.4.26", "vue": "^3.4.26",
"vue-echarts": "^6.6.9", "vue-echarts": "^6.6.9",
"vue-router": "^4.3.2", "vue-router": "^4.3.2",
......
import httpRequest from '@/utils/axios'
// 旅程生成用户事件数据列表(搜索条件)
export function getGenerateListFilter() {
return httpRequest.get('/api/lab/v1/experiment/member/itinerary-generate-data-condition')
}
// 旅程生成用户事件数据列表
export function getGenerateList(params: any) {
return httpRequest.get('/api/lab/v1/experiment/member/itinerary-generate-data', { params })
}
...@@ -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;
}
This source diff could not be displayed because it is too large. You can view the blob instead.
<script setup>
import AppList from '@/components/base/AppList.vue'
import { getGenerateListFilter, getGenerateList } from '@/api/generateEvent'
const ViewEvent = defineAsyncComponent(() => import('@/components/ViewEvent.vue'))
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
const filter = reactive({ students: [], connections: [], events: [] })
async function fetchListFilters() {
const { data } = await getGenerateListFilter()
Object.assign(filter, data)
}
onMounted(() => {
fetchListFilters()
})
const students = computed(() => {
return filter.students.map((item) => {
return { ...item, name: `${item.name}/${item.mobile}` }
})
})
// 列表配置
const listOptions = computed(() => {
const userId = userStore.role.id == 1 ? userStore.user?.id : ''
return {
remote: {
httpRequest: getGenerateList,
params: { created_operator: userId, name: '', connection_id: '', experiment_meta_event_id: '', created_time_start: '', created_time_end: '' }
},
filters: [
{
type: 'select',
prop: 'created_operator',
placeholder: '请选择学生姓名',
options: students.value,
labelKey: 'name',
valueKey: 'sso_id',
disabled: !!userId
},
{ type: 'input', prop: 'name', placeholder: '请输入用户姓名' },
{ type: 'select', prop: 'connection_id', placeholder: '请选择来源连接', options: filter.connections, labelKey: 'type_name', valueKey: 'id' },
{ type: 'select', prop: 'experiment_meta_event_id', placeholder: '请选择事件', options: filter.events, labelKey: 'name', valueKey: 'id' },
{ type: 'input', prop: 'created_time_start', slots: 'filter-start' },
{ type: 'input', prop: 'created_time_end', slots: 'filter-end' }
],
columns: [
{ label: '序号', type: 'index', width: 60 },
{ label: '连接', prop: 'connection_name' },
{ label: '事件名称', prop: 'event_name' },
{ label: '用户名称', prop: 'member.name' },
{ label: '手机号', prop: 'member.mobile' },
{ label: '事件发生时间', prop: 'created_time' },
{ label: '操作', slots: 'table-x', width: 220 }
]
}
})
const viewEventVisible = ref(false)
const currentViewEvent = ref()
function handleViewEvent(item) {
viewEventVisible.value = true
currentViewEvent.value = item
}
</script>
<template>
<el-dialog title="用户事件数据" width="1000px">
<AppList v-bind="listOptions" ref="appList">
<template #filter-start="{ params }">
<el-date-picker v-model="params.created_time_start" type="datetime" placeholder="请选择事件发生起始时间" value-format="YYYY-MM-DD HH:mm:ss" />
</template>
<template #filter-end="{ params }">
<el-date-picker v-model="params.created_time_end" type="datetime" placeholder="请选择事件发生截止时间" value-format="YYYY-MM-DD HH:mm:ss" />
</template>
<template #table-x="{ row }">
<el-button type="primary" plain @click="handleViewEvent(row)">事件详情</el-button>
<el-button type="primary" plain>
<router-link target="_blank" :to="{ path: '/user/image', query: { user_id: row.member.id, experiment_id: row.experiment_id } }">用户详情</router-link>
</el-button>
</template>
</AppList>
<!-- 事件详情 -->
<ViewEvent v-model="viewEventVisible" :event="currentViewEvent" :user="currentViewEvent.member" v-if="viewEventVisible && currentViewEvent"></ViewEvent>
</el-dialog>
</template>
...@@ -51,7 +51,7 @@ const listOptions = computed(() => { ...@@ -51,7 +51,7 @@ const listOptions = computed(() => {
</script> </script>
<template> <template>
<el-dialog title="标签用户" width="1000px"> <el-dialog title="群组用户" width="1000px">
<AppList v-bind="listOptions" ref="appList"> <AppList v-bind="listOptions" ref="appList">
<template #table-x="{ row }"> <template #table-x="{ row }">
<el-button type="primary" plain> <el-button type="primary" plain>
......
...@@ -51,7 +51,7 @@ const listOptions = computed(() => { ...@@ -51,7 +51,7 @@ const listOptions = computed(() => {
</script> </script>
<template> <template>
<el-dialog title="群组用户" width="1000px"> <el-dialog title="标签用户" width="1000px">
<AppList v-bind="listOptions" ref="appList"> <AppList v-bind="listOptions" ref="appList">
<template #table-x="{ row }"> <template #table-x="{ row }">
<el-button type="primary" plain> <el-button type="primary" plain>
......
...@@ -5,7 +5,6 @@ const styleHeight = computed(() => { ...@@ -5,7 +5,6 @@ const styleHeight = computed(() => {
}) })
</script> </script>
<template> <template>
<div class="app-card"> <div class="app-card">
<div class="app-card-hd"> <div class="app-card-hd">
...@@ -24,6 +23,8 @@ const styleHeight = computed(() => { ...@@ -24,6 +23,8 @@ const styleHeight = computed(() => {
<style lang="scss"> <style lang="scss">
.app-card { .app-card {
display: flex;
flex-direction: column;
min-height: v-bind(styleHeight); min-height: v-bind(styleHeight);
background: #fff; background: #fff;
box-shadow: 0 1px 6px 0 rgb(228 232 235 / 20%); box-shadow: 0 1px 6px 0 rgb(228 232 235 / 20%);
...@@ -49,4 +50,21 @@ const styleHeight = computed(() => { ...@@ -49,4 +50,21 @@ const styleHeight = computed(() => {
font-size: 14px; font-size: 14px;
} }
} }
.app-card-bd {
flex: 1;
overflow-x: hidden;
overflow-y: auto;
}
.h2-title {
padding-left: 5px;
font-size: 18px;
font-weight: 500;
line-height: 1;
color: #aa1941;
margin: 20px 0;
border-left: 3px solid #aa1941;
display: flex;
align-items: center;
justify-content: space-between;
}
</style> </style>
...@@ -150,14 +150,15 @@ const handlePreview: UploadProps['onPreview'] = uploadFile => { ...@@ -150,14 +150,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%;
...@@ -173,4 +174,7 @@ const handlePreview: UploadProps['onPreview'] = uploadFile => { ...@@ -173,4 +174,7 @@ const handlePreview: UploadProps['onPreview'] = uploadFile => {
height: 100%; height: 100%;
text-align: center; text-align: center;
} }
.el-upload__tip:empty {
display: none;
}
</style> </style>
...@@ -27,12 +27,15 @@ const component = computed(() => { ...@@ -27,12 +27,15 @@ const component = computed(() => {
MAWeibo: markRaw(defineAsyncComponent(() => import('./components/marketingAction/weibo/Index.vue'))), MAWeibo: markRaw(defineAsyncComponent(() => import('./components/marketingAction/weibo/Index.vue'))),
MADingTalk: markRaw(defineAsyncComponent(() => import('./components/marketingAction/dingtalk/Index.vue'))), MADingTalk: markRaw(defineAsyncComponent(() => import('./components/marketingAction/dingtalk/Index.vue'))),
MAAB: markRaw(defineAsyncComponent(() => import('./components/marketingAction/ab/Index.vue'))), MAAB: markRaw(defineAsyncComponent(() => import('./components/marketingAction/ab/Index.vue'))),
MAXiaohongshu: markRaw(defineAsyncComponent(() => import('./components/marketingAction/xiaohongshu/Index.vue'))),
CBAttributeJudgment: markRaw(defineAsyncComponent(() => import('./components/conditionalBranch/attributeJudgment/Index.vue'))), CBAttributeJudgment: markRaw(defineAsyncComponent(() => import('./components/conditionalBranch/attributeJudgment/Index.vue'))),
CBGroupJudgment: markRaw(defineAsyncComponent(() => import('./components/conditionalBranch/groupJudgment/Index.vue'))), CBGroupJudgment: markRaw(defineAsyncComponent(() => import('./components/conditionalBranch/groupJudgment/Index.vue'))),
CBEventJudgment: markRaw(defineAsyncComponent(() => import('./components/conditionalBranch/eventJudgment/Index.vue'))), CBEventJudgment: markRaw(defineAsyncComponent(() => import('./components/conditionalBranch/eventJudgment/Index.vue'))),
CBTimeJudgment: markRaw(defineAsyncComponent(() => import('./components/conditionalBranch/timeJudgment/Index.vue'))), CBTimeJudgment: markRaw(defineAsyncComponent(() => import('./components/conditionalBranch/timeJudgment/Index.vue'))),
CBOffiaccount: markRaw(defineAsyncComponent(() => import('./components/conditionalBranch/offiaccount/Index.vue'))), CBOffiaccount: markRaw(defineAsyncComponent(() => import('./components/conditionalBranch/offiaccount/Index.vue'))),
CBLabelJudgment: markRaw(defineAsyncComponent(() => import('./components/conditionalBranch/labelJudgment/Index.vue'))) CBLabelJudgment: markRaw(defineAsyncComponent(() => import('./components/conditionalBranch/labelJudgment/Index.vue'))),
CBXiaohongshu: markRaw(defineAsyncComponent(() => import('./components/conditionalBranch/xiaohongshu/Index.vue'))),
CBDouyin: markRaw(defineAsyncComponent(() => import('./components/conditionalBranch/douyin/Index.vue')))
} }
return allComponent[props.node?.data.component_name || props.node?.data.componentName] return allComponent[props.node?.data.component_name || props.node?.data.componentName]
}) })
......
...@@ -15,6 +15,7 @@ const connections = computed(() => { ...@@ -15,6 +15,7 @@ const connections = computed(() => {
// return connectionList.value.filter(item => props.connectionIds?.includes(item.id)) // return connectionList.value.filter(item => props.connectionIds?.includes(item.id))
}) })
// component_type https://gitlab-pro.ezijing.com/ezijing-server/api-documents/blob/master/%E7%BB%9F%E4%B8%80%E8%B5%84%E6%BA%90%E7%AE%A1%E7%90%86%E7%B3%BB%E7%BB%9F/DML-%E5%AE%9E%E9%AA%8C%E5%B9%B3%E5%8F%B0%E7%AE%A1%E7%90%86-%E5%AE%9E%E9%AA%8C%E6%95%B0%E6%8D%AE%E7%AE%A1%E7%90%86.md#%E8%8E%B7%E5%8F%96%E5%AE%9E%E9%AA%8C%E6%97%85%E7%A8%8B%E6%A3%80%E7%B4%A2%E7%94%A8%E6%88%B7%E7%9A%84%E7%BB%93%E6%9E%9C
const list = ref([ const list = ref([
{ {
name: '触发条件', name: '触发条件',
...@@ -151,7 +152,7 @@ const list = ref([ ...@@ -151,7 +152,7 @@ const list = ref([
type: 2, type: 2,
type_name: '营销动作', type_name: '营销动作',
icon: '18', icon: '18',
component_type: 2, component_type: 4,
component_name: 'MADelayProcess' component_name: 'MADelayProcess'
}, },
{ {
...@@ -198,7 +199,7 @@ const list = ref([ ...@@ -198,7 +199,7 @@ const list = ref([
component_name: 'MADouyin', component_name: 'MADouyin',
connection_type: 6 connection_type: 6
}, },
// { name: '小红书', type: 2, type_name: '营销动作', icon: '8',component_type:2, component_name: 'MAXiaohongshu', connection_type: 8 }, { name: '小红书', type: 2, type_name: '营销动作', icon: '8', component_type: 13, component_name: 'MAXiaohongshu', connection_type: 8 },
{ {
name: '微博', name: '微博',
type: 2, type: 2,
...@@ -279,6 +280,24 @@ const list = ref([ ...@@ -279,6 +280,24 @@ const list = ref([
component_type: 6, component_type: 6,
component_name: 'CBOffiaccount', component_name: 'CBOffiaccount',
connection_type: 1 connection_type: 1
},
{
name: '小红书',
type: 3,
type_name: '条件分支',
icon: '8',
component_type: 7,
component_name: 'CBXiaohongshu',
connection_type: 8
},
{
name: '抖音',
type: 3,
type_name: '条件分支',
icon: '6',
component_type: 8,
component_name: 'CBDouyin',
connection_type: 6
} }
] ]
} }
......
...@@ -56,7 +56,7 @@ function updateNode() { ...@@ -56,7 +56,7 @@ function updateNode() {
</script> </script>
<template> <template>
<el-dialog title="设置组件" append-to-body width="600px"> <el-dialog title="设置组件" append-to-body width="800px">
<el-form ref="formRef" :model="form" :rules="rules" label-suffix=":"> <el-form ref="formRef" :model="form" :rules="rules" label-suffix=":">
<!-- 学生设置组件 --> <!-- 学生设置组件 -->
<template v-if="role === 'student'"> <template v-if="role === 'student'">
...@@ -102,19 +102,11 @@ function updateNode() { ...@@ -102,19 +102,11 @@ function updateNode() {
<!-- <el-button plain auto-insert-space @click="$emit('update:modelValue', false)">关闭</el-button> --> <!-- <el-button plain auto-insert-space @click="$emit('update:modelValue', false)">关闭</el-button> -->
<el-button @click="step--" plain auto-insert-space v-if="step >= 1">上一步</el-button> <el-button @click="step--" plain auto-insert-space v-if="step >= 1">上一步</el-button>
<el-button @click="step++" plain auto-insert-space v-if="step < stepNum">下一步</el-button> <el-button @click="step++" plain auto-insert-space v-if="step < stepNum">下一步</el-button>
<el-button <el-button type="primary" auto-insert-space @click="submit().then(() => $emit('update:modelValue', false))" v-if="step === stepNum">保存</el-button>
type="primary"
auto-insert-space
@click="submit().then(() => $emit('update:modelValue', false))"
v-if="step === stepNum"
>保存</el-button
>
</el-row> </el-row>
<el-row justify="center" v-else> <el-row justify="center" v-else>
<el-button plain auto-insert-space @click="$emit('update:modelValue', false)">关闭</el-button> <el-button plain auto-insert-space @click="$emit('update:modelValue', false)">关闭</el-button>
<el-button type="primary" auto-insert-space @click="submit().then(() => $emit('update:modelValue', false))" <el-button type="primary" auto-insert-space @click="submit().then(() => $emit('update:modelValue', false))">保存</el-button>
>保存</el-button
>
</el-row> </el-row>
</template> </template>
</el-dialog> </el-dialog>
......
...@@ -38,7 +38,7 @@ function updateNode() { ...@@ -38,7 +38,7 @@ function updateNode() {
</script> </script>
<template> <template>
<el-dialog title="组件配置" append-to-body width="600px"> <el-dialog title="组件配置" append-to-body width="800px">
<el-form ref="formRef" :model="form" :rules="rules" label-suffix=":"> <el-form ref="formRef" :model="form" :rules="rules" label-suffix=":">
<el-row justify="space-between"> <el-row justify="space-between">
<el-form-item label="组件类型">{{ node.data.type_name }}</el-form-item> <el-form-item label="组件类型">{{ node.data.type_name }}</el-form-item>
...@@ -67,9 +67,7 @@ function updateNode() { ...@@ -67,9 +67,7 @@ function updateNode() {
<template #footer> <template #footer>
<el-row justify="center"> <el-row justify="center">
<el-button plain auto-insert-space @click="$emit('update:modelValue', false)">关闭</el-button> <el-button plain auto-insert-space @click="$emit('update:modelValue', false)">关闭</el-button>
<el-button type="primary" auto-insert-space @click="submit" v-if="role === 'teacher' && templateType === '2'" <el-button type="primary" auto-insert-space @click="submit" v-if="role === 'teacher' && templateType === '2'">保存</el-button>
>保存</el-button
>
</el-row> </el-row>
</template> </template>
</el-dialog> </el-dialog>
......
...@@ -28,26 +28,26 @@ function onRemove() { ...@@ -28,26 +28,26 @@ function onRemove() {
<div class="custom-node__inner"><slot :node="node" /></div> <div class="custom-node__inner"><slot :node="node" /></div>
<p class="node-label">{{ node.label }}</p> <p class="node-label">{{ node.label }}</p>
</div> </div>
<Handle id="handle-left" class="handle" :position="Position.Left" :connectable="false" /> <Handle id="handle-left" class="handle" :position="Position.Left" type="target" />
<Handle id="handle-top" class="handle" :position="Position.Top" :connectable="false" /> <Handle id="handle-top" class="handle" :position="Position.Top" type="target" />
<Handle id="handle-bottom" class="handle" :position="Position.Bottom" :connectable="false" /> <Handle id="handle-bottom" class="handle" :position="Position.Bottom" type="target" />
<Handle id="handle-right" class="handle" :position="Position.Right" :connectable="false" /> <Handle id="handle-right" class="handle" :position="Position.Right" type="target" />
<div class="handle-wrap" v-if="!!(nodesConnectable && canConnect)"> <div class="handle-wrap" v-if="!!(nodesConnectable && canConnect)">
<template v-if="connectionType === 1"> <template v-if="connectionType === 1">
<Handle id="handle-yes" class="handle-link is-yes" :position="Position.Left"></Handle> <Handle id="handle-yes" class="handle-link is-yes" :position="Position.Left" type="source"></Handle>
<Handle id="handle-no" class="handle-link is-no" :position="Position.Left"></Handle> <Handle id="handle-no" class="handle-link is-no" :position="Position.Left" type="source"></Handle>
</template> </template>
<template v-else-if="connectionType === 2"> <template v-else-if="connectionType === 2">
<Handle id="handle-success" class="handle-link is-yes" :position="Position.Left">成功</Handle> <Handle id="handle-success" class="handle-link is-yes" :position="Position.Left" type="source">成功</Handle>
<Handle id="handle-failure" class="handle-link is-no" :position="Position.Left">失败</Handle> <Handle id="handle-failure" class="handle-link is-no" :position="Position.Left" type="source">失败</Handle>
<Handle id="handle-any" class="handle-link is-any" :position="Position.Left">继续</Handle> <Handle id="handle-any" class="handle-link is-any" :position="Position.Left" type="source">继续</Handle>
</template> </template>
<template v-else-if="connectionType === 3"> <template v-else-if="connectionType === 3">
<Handle id="handle-a" class="handle-link is-yes" :position="Position.Left">A</Handle> <Handle id="handle-a" class="handle-link is-yes" :position="Position.Left" type="source">A</Handle>
<Handle id="handle-b" class="handle-link is-no" :position="Position.Left">B</Handle> <Handle id="handle-b" class="handle-link is-no" :position="Position.Left" type="source">B</Handle>
</template> </template>
<template v-else> <template v-else>
<Handle id="handle-any" class="handle-default" :position="Position.Left"> <Handle id="handle-any" class="handle-default" :position="Position.Left" type="source">
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="styles__StyledSVGIconPathComponent-sc-16fsqc8-0 dGjRCM svg-icon-path-icon fill" class="styles__StyledSVGIconPathComponent-sc-16fsqc8-0 dGjRCM svg-icon-path-icon fill"
...@@ -98,6 +98,7 @@ function onRemove() { ...@@ -98,6 +98,7 @@ function onRemove() {
} }
.handle-default { .handle-default {
left: 0; left: 0;
transform: translateY(-50%);
svg { svg {
pointer-events: none; pointer-events: none;
} }
......
...@@ -8,7 +8,7 @@ const props = defineProps<{ node: any }>() ...@@ -8,7 +8,7 @@ const props = defineProps<{ node: any }>()
const role = inject('role') as string const role = inject('role') as string
const form = reactive({ const form: any = reactive({
rules: [{ attr_id: '', attr_type: '', operate: '', value: '' }] rules: [{ attr_id: '', attr_type: '', operate: '', value: '' }]
}) })
watchEffect(() => { watchEffect(() => {
...@@ -33,7 +33,7 @@ function onAttrChange(rule: any) { ...@@ -33,7 +33,7 @@ function onAttrChange(rule: any) {
} }
function addRule() { function addRule() {
form.rules.push({ attr_id: '', attr_type: '', operate: '=', value: '' }) form.rules.push({ attr_id: '', attr_type: '', operate: '', value: '' })
} }
function removeRule(index: number) { function removeRule(index: number) {
...@@ -43,6 +43,15 @@ function removeRule(index: number) { ...@@ -43,6 +43,15 @@ function removeRule(index: number) {
function getAttr(attrId: string) { function getAttr(attrId: string) {
return userAttrList.value.find(item => item.id === attrId) return userAttrList.value.find(item => item.id === attrId)
} }
// 条件改变
function handleOperateChange(value: string, item: any) {
item.value = ''
// 区间
if (value === 'range') {
item.value = { start: undefined, end: undefined }
}
}
</script> </script>
<template> <template>
...@@ -52,27 +61,39 @@ function getAttr(attrId: string) { ...@@ -52,27 +61,39 @@ function getAttr(attrId: string) {
<el-select v-model="rule.attr_id" @change="onAttrChange(rule)" placeholder="请选择属性" style="width: 120px"> <el-select v-model="rule.attr_id" @change="onAttrChange(rule)" placeholder="请选择属性" style="width: 120px">
<el-option :key="item.id" v-for="item in userAttrList" :value="item.id" :label="item.name"></el-option> <el-option :key="item.id" v-for="item in userAttrList" :value="item.id" :label="item.name"></el-option>
</el-select> </el-select>
<el-select v-model="rule.operate" style="margin: 0 10px; width: 120px"> <el-select v-model="rule.operate" style="margin: 0 10px; width: 120px" @change="value => handleOperateChange(value, rule)">
<el-option <el-option v-for="item in getOperatorList(rule.attr_type)" :key="item.value" :value="item.value" :label="item.label"></el-option>
v-for="item in getOperatorList(rule.attr_type)"
:key="item.value"
:value="item.value"
:label="item.label"></el-option>
</el-select> </el-select>
<el-input <el-form-item v-if="!['null', 'not null'].includes(rule.operate)">
v-model="rule.value" <!-- 数字区间 -->
placeholder="请输入属性值" <template v-if="['2', '3'].includes(rule.attr_type) && rule.operate === 'range'">
:maxlength="getAttr(rule.attr_id)?.format" <el-input-number step-strictly :controls="false" :min="0" v-model="rule.value.start" />
style="width: 180px"></el-input> <el-input-number step-strictly :controls="false" :min="0" v-model="rule.value.end" style="margin-left: 10px" />
</template>
<!-- 日期区间 -->
<template v-else-if="rule.attr_type === '4' && rule.operate === 'range'">
<el-date-picker v-model="rule.value.start" type="date" value-format="YYYY-MM-DD" style="width: 180px" />
<el-date-picker v-model="rule.value.end" type="date" value-format="YYYY-MM-DD" style="width: 180px; margin-left: 10px" />
</template>
<!-- 时间区间 -->
<template v-else-if="rule.attr_type === '5' && rule.operate === 'range'">
<el-date-picker v-model="rule.value.start" type="datetime" value-format="YYYY-MM-DD HH:mm:ss" style="width: 180px" />
<el-date-picker v-model="rule.value.end" type="datetime" value-format="YYYY-MM-DD HH:mm:ss" style="width: 180px; margin-left: 10px" />
</template>
<template v-else-if="rule.attr_type === '4' && (rule.operate === 'after' || rule.operate === 'before')">
<el-date-picker v-model="rule.value" type="date" value-format="YYYY-MM-DD" />
</template>
<template v-else-if="rule.attr_type === '5' && (rule.operate === 'after' || rule.operate === 'before')">
<el-date-picker v-model="rule.value" type="datetime" value-format="YYYY-MM-DD HH:mm:ss" style="width: 180px; margin-left: 10px" />
</template>
<template v-else>
<el-input v-model="rule.value" placeholder="请输入属性值" :maxlength="getAttr(rule.attr_id)?.format" style="width: 180px"></el-input>
</template>
</el-form-item>
<el-icon style="margin-left: 10px" size="20" color="#cf5b78" @click="removeRule(index)" v-if="index !== 0"> <el-icon style="margin-left: 10px" size="20" color="#cf5b78" @click="removeRule(index)" v-if="index !== 0">
<RemoveFilled /> <RemoveFilled />
</el-icon> </el-icon>
<el-icon <el-icon style="margin-left: 10px" size="20" color="#cf5b78" @click="addRule" v-if="index === form.rules.length - 1">
style="margin-left: 10px"
size="20"
color="#cf5b78"
@click="addRule"
v-if="index === form.rules.length - 1">
<CirclePlusFilled /> <CirclePlusFilled />
</el-icon> </el-icon>
</el-row> </el-row>
......
<script setup lang="ts">
import ConfigTemplate from '../../ConfigTemplate.vue'
import { useConnection } from '../../../composables/useAllData'
const props = defineProps<{ node: any }>()
const role = inject('role') as string
const form = reactive({
operate: '',
operate_name: '',
connection_id: '',
connection_name: ''
})
watchEffect(() => {
Object.assign(form, props.node.data[role])
})
const { connectionList } = useConnection(6)
const operateList = ref([{ label: '关注中', value: '0' }])
watchEffect(() => {
form.operate_name = operateList.value.find(item => item.value === form.operate)?.label || ''
})
watchEffect(() => {
form.connection_name = connectionList.value.find(item => item.id === form.connection_id)?.name || ''
})
</script>
<template>
<ConfigTemplate :model="form" :node="node" :stepNum="1">
<template #default="{ step }: { step: number }">
<el-form-item>
<template v-if="step === 0">
<el-radio-group v-model="form.operate" v-if="step === 0">
<el-radio v-for="item in operateList" :key="item.value" :value="item.value" style="width: 105px">
{{ item.label }}
</el-radio>
</el-radio-group>
</template>
<template v-else-if="step === 1">
<el-select placeholder="请选择关联使用抖音" style="width: 100%" v-model="form.connection_id">
<el-option v-for="item in connectionList" :key="item.id" :value="item.id" :label="item.name"></el-option>
</el-select>
</template>
</el-form-item>
</template>
</ConfigTemplate>
</template>
<script setup lang="ts">
import ConfigViewTemplate from '../../ConfigViewTemplate.vue'
import { useConnection } from '../../../composables/useAllData'
const role = inject('role') as string
defineProps<{ node: any }>()
const operateList = [{ label: '关注中', value: '0' }]
const { connectionList } = useConnection()
const getConnectionLabel = function (id: string) {
return connectionList.value.find(item => item.id === id)?.name || ''
}
</script>
<template>
<ConfigViewTemplate :node="node">
<el-form-item :label="role === 'student' ? '我的答案' : '学生答案'">
{{ operateList.find(item => item.value === node.data.student?.operate)?.label || '' }}
{{ getConnectionLabel(node.data.student?.connection_id) }}
</el-form-item>
<el-form-item label="正确答案">
{{ operateList.find(item => item.value === node.data.teacher?.operate)?.label || '' }}
{{ getConnectionLabel(node.data.teacher?.connection_id) }}
</el-form-item>
</ConfigViewTemplate>
</template>
<!-- 公众号 -->
<script setup lang="ts">
import NodeTemplate from '../../NodeTemplate.vue'
import Icon from '@/components/ConnectionIcon.vue'
const Config = defineAsyncComponent(() => import('./Config.vue'))
const ConfigView = defineAsyncComponent(() => import('./ConfigView.vue'))
const Rule = defineAsyncComponent(() => import('./Rule.vue'))
const props = defineProps<{ node: any }>()
const action = inject('action') as string
const role = inject('role') as string
const templateType = inject('templateType') as string
// 是否置灰
const isGray = computed(() => {
return templateType === '2' && role === 'student' && action === 'edit' && !props.node.data[role]
})
// 设置
const settingVisible = ref(false)
</script>
<template>
<NodeTemplate :node="node" :connectionType="1" @setting="settingVisible = true">
<div class="node-item">
<Icon class="circle" name="hexagon" :color="isGray ? '#9a9a9a' : '#ceaa62'" w="60" h="60"></Icon>
<Icon class="icon" name="6" color="#fff" w="24" h="24"></Icon>
</div>
</NodeTemplate>
<!-- 配置 -->
<Config v-model="settingVisible" :node="node" v-if="settingVisible && action === 'edit'" />
<!-- 查看配置 -->
<ConfigView v-model="settingVisible" :node="node" v-if="settingVisible && action === 'view'" />
<!-- 数据生成规则 -->
<Rule v-model="settingVisible" :node="node" v-if="settingVisible && action === 'rule'" />
</template>
<script setup lang="ts">
import RuleTemplate from '../../RuleTemplate.vue'
import { useConnection } from '../../../composables/useAllData'
const { getConnection } = useConnection()
const props = defineProps<{ node: any }>()
const config = computed(() => {
return props.node.data.teacher || {}
})
const connection = computed(() => {
return getConnection(config.value?.connection_id)
})
</script>
<template>
<RuleTemplate :node="node" step>
<template #header-answer>
连接:<span class="is-answer">{{ connection?.name }}</span> &nbsp;&nbsp; 事件:<span class="is-answer">关注抖音</span>
</template>
</RuleTemplate>
</template>
...@@ -52,12 +52,7 @@ watchEffect(() => { ...@@ -52,12 +52,7 @@ watchEffect(() => {
<el-select v-model="form.date_rule" placeholder="请选择" style="width: 115px; margin-right: 10px"> <el-select v-model="form.date_rule" placeholder="请选择" style="width: 115px; margin-right: 10px">
<el-option v-for="item in dateRuleList" :key="item.value" :label="item.label" :value="item.value" /> <el-option v-for="item in dateRuleList" :key="item.value" :label="item.label" :value="item.value" />
</el-select> </el-select>
<el-date-picker <el-date-picker v-if="['0', '1'].includes(form.date_rule)" v-model="form.date" type="date" placeholder="请选择" value-format="YYYY-MM-DD" />
v-if="['0', '1'].includes(form.date_rule)"
v-model="form.date"
type="date"
placeholder="请选择"
value-format="YYYY-MM-DD" />
<el-date-picker <el-date-picker
v-else v-else
v-model="form.date" v-model="form.date"
...@@ -69,7 +64,7 @@ watchEffect(() => { ...@@ -69,7 +64,7 @@ watchEffect(() => {
</template> </template>
<template v-else> <template v-else>
<el-checkbox-group v-model="form.week"> <el-checkbox-group v-model="form.week">
<el-checkbox v-for="item in weekList" :key="item" :value="item" /> <el-checkbox v-for="item in weekList" :key="item" :label="item" :value="item" />
</el-checkbox-group> </el-checkbox-group>
</template> </template>
</el-form-item> </el-form-item>
......
<script setup lang="ts">
import ConfigTemplate from '../../ConfigTemplate.vue'
import { useConnection } from '../../../composables/useAllData'
const props = defineProps<{ node: any }>()
const role = inject('role') as string
const form = reactive({
operate: '',
operate_name: '',
connection_id: '',
connection_name: ''
})
watchEffect(() => {
Object.assign(form, props.node.data[role])
})
const { connectionList } = useConnection(8)
const operateList = ref([{ label: '关注中', value: '0' }])
watchEffect(() => {
form.operate_name = operateList.value.find(item => item.value === form.operate)?.label || ''
})
watchEffect(() => {
form.connection_name = connectionList.value.find(item => item.id === form.connection_id)?.name || ''
})
</script>
<template>
<ConfigTemplate :model="form" :node="node" :stepNum="1">
<template #default="{ step }: { step: number }">
<el-form-item>
<template v-if="step === 0">
<el-radio-group v-model="form.operate" v-if="step === 0">
<el-radio v-for="item in operateList" :key="item.value" :value="item.value" style="width: 105px">
{{ item.label }}
</el-radio>
</el-radio-group>
</template>
<template v-else-if="step === 1">
<el-select placeholder="请选择关联使用小红书" style="width: 100%" v-model="form.connection_id">
<el-option v-for="item in connectionList" :key="item.id" :value="item.id" :label="item.name"></el-option>
</el-select>
</template>
</el-form-item>
</template>
</ConfigTemplate>
</template>
<script setup lang="ts">
import ConfigViewTemplate from '../../ConfigViewTemplate.vue'
import { useConnection } from '../../../composables/useAllData'
const role = inject('role') as string
defineProps<{ node: any }>()
const operateList = [{ label: '关注中', value: '0' }]
const { connectionList } = useConnection()
const getConnectionLabel = function (id: string) {
return connectionList.value.find(item => item.id === id)?.name || ''
}
</script>
<template>
<ConfigViewTemplate :node="node">
<el-form-item :label="role === 'student' ? '我的答案' : '学生答案'">
{{ operateList.find(item => item.value === node.data.student?.operate)?.label || '' }}
{{ getConnectionLabel(node.data.student?.connection_id) }}
</el-form-item>
<el-form-item label="正确答案">
{{ operateList.find(item => item.value === node.data.teacher?.operate)?.label || '' }}
{{ getConnectionLabel(node.data.teacher?.connection_id) }}
</el-form-item>
</ConfigViewTemplate>
</template>
<!-- 公众号 -->
<script setup lang="ts">
import NodeTemplate from '../../NodeTemplate.vue'
import Icon from '@/components/ConnectionIcon.vue'
const Config = defineAsyncComponent(() => import('./Config.vue'))
const ConfigView = defineAsyncComponent(() => import('./ConfigView.vue'))
const Rule = defineAsyncComponent(() => import('./Rule.vue'))
const props = defineProps<{ node: any }>()
const action = inject('action') as string
const role = inject('role') as string
const templateType = inject('templateType') as string
// 是否置灰
const isGray = computed(() => {
return templateType === '2' && role === 'student' && action === 'edit' && !props.node.data[role]
})
// 设置
const settingVisible = ref(false)
</script>
<template>
<NodeTemplate :node="node" :connectionType="1" @setting="settingVisible = true">
<div class="node-item">
<Icon class="circle" name="hexagon" :color="isGray ? '#9a9a9a' : '#ceaa62'" w="60" h="60"></Icon>
<Icon class="icon" name="8" color="#fff" w="24" h="24"></Icon>
</div>
</NodeTemplate>
<!-- 配置 -->
<Config v-model="settingVisible" :node="node" v-if="settingVisible && action === 'edit'" />
<!-- 查看配置 -->
<ConfigView v-model="settingVisible" :node="node" v-if="settingVisible && action === 'view'" />
<!-- 数据生成规则 -->
<Rule v-model="settingVisible" :node="node" v-if="settingVisible && action === 'rule'" />
</template>
<script setup lang="ts">
import RuleTemplate from '../../RuleTemplate.vue'
import { useConnection } from '../../../composables/useAllData'
const { getConnection } = useConnection()
const props = defineProps<{ node: any }>()
const config = computed(() => {
return props.node.data.teacher || {}
})
const connection = computed(() => {
return getConnection(config.value?.connection_id)
})
</script>
<template>
<RuleTemplate :node="node" step>
<template #header-answer>
连接:<span class="is-answer">{{ connection?.name }}</span> &nbsp;&nbsp; 事件:<span class="is-answer"
>关注小红书</span
>
</template>
</RuleTemplate>
</template>
<script setup lang="ts">
import ConfigTemplate from '../../ConfigTemplate.vue'
import { useMaterial, useConnection } from '../../../composables/useAllData'
import { useMapStore } from '@/stores/map'
const props = defineProps<{ node: any }>()
const role = inject('role') as string
const form = reactive({
operate: '',
material_type: '',
material_id: '',
connection_id: ''
})
watchEffect(() => {
Object.assign(form, props.node.data[role])
})
const operateList = ref([
{ label: '向用户发送文本私信', value: '0' },
{ label: '向用户发送图片私信', value: '1' },
{ label: '向用户发送视频私信', value: '2' }
])
const materialTypeList = useMapStore().getMapValuesByKey('experiment_marketing_material_type')
const { materialList, materialType } = useMaterial()
watchEffect(() => {
materialType.value = form.material_type
})
const { connectionList } = useConnection(8)
</script>
<template>
<ConfigTemplate :model="form" :node="node" :stepNum="2">
<template #default="{ step }: { step: number }">
<template v-if="step === 0">
<el-form-item>
<el-radio-group v-model="form.operate" v-if="step === 0">
<el-radio v-for="item in operateList" :key="item.value" :value="item.value">
{{ item.label }}
</el-radio>
</el-radio-group>
</el-form-item>
</template>
<template v-else-if="step === 1">
<el-form-item>
<el-select placeholder="请选择" style="width: 100%" v-model="form.connection_id">
<el-option v-for="item in connectionList" :key="item.id" :value="item.id" :label="item.name"></el-option>
</el-select>
</el-form-item>
</template>
<template v-else-if="step === 2">
<el-form-item>
<el-radio-group v-model="form.material_type" @change="form.material_id = ''">
<el-radio v-for="item in materialTypeList" :key="item.id" :value="item.value" style="width: 105px">
发送{{ item.label }}
</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item>
<el-select placeholder="请选择资料内容" v-model="form.material_id" style="width: 100%">
<el-option v-for="item in materialList" :key="item.id" :value="item.id" :label="item.name"></el-option>
</el-select>
</el-form-item>
</template>
</template>
</ConfigTemplate>
</template>
<script setup lang="ts">
import ConfigViewTemplate from '../../ConfigViewTemplate.vue'
import { useMaterial, useConnection } from '../../../composables/useAllData'
import { useMapStore } from '@/stores/map'
const role = inject('role') as string
defineProps<{ node: any }>()
const operateList = ref([
{ label: '向用户发送文本私信', value: '0' },
{ label: '向用户发送图片私信', value: '1' },
{ label: '向用户发送视频私信', value: '2' }
])
const { connectionList } = useConnection()
const getConnectionLabel = function (id: string) {
return connectionList.value.find(item => item.id === id)?.name || ''
}
const materialTypeList = useMapStore().getMapValuesByKey('experiment_marketing_material_type')
const { materialList } = useMaterial()
</script>
<template>
<ConfigViewTemplate :node="node">
<el-form-item :label="role === 'student' ? '我的答案' : '学生答案'">
{{ operateList.find(item => item.value === node.data.student?.operate)?.label || '' }}
{{ getConnectionLabel(node.data.student?.connection_id) }}
发送{{ materialTypeList.find(item => item.value === node.data.student?.material_type)?.label }}
{{ materialList.find(item => item.id === node.data.student?.material_id)?.name }}
</el-form-item>
<el-form-item label="正确答案">
{{ operateList.find(item => item.value === node.data.teacher?.operate)?.label || '' }}
{{ getConnectionLabel(node.data.teacher?.connection_id) }}
发送{{ materialTypeList.find(item => item.value === node.data.teacher?.material_type)?.label }}
{{ materialList.find(item => item.id === node.data.teacher?.material_id)?.name }}
</el-form-item>
</ConfigViewTemplate>
</template>
<!-- 抖音 -->
<script setup lang="ts">
import NodeTemplate from '../../NodeTemplate.vue'
import Icon from '@/components/ConnectionIcon.vue'
const Config = defineAsyncComponent(() => import('./Config.vue'))
const ConfigView = defineAsyncComponent(() => import('./ConfigView.vue'))
const Rule = defineAsyncComponent(() => import('./Rule.vue'))
const props = defineProps<{ node: any }>()
const action = inject('action') as string
const role = inject('role') as string
const templateType = inject('templateType') as string
// 是否置灰
const isGray = computed(() => {
return templateType === '2' && role === 'student' && action === 'edit' && !props.node.data[role]
})
// 设置
const settingVisible = ref(false)
</script>
<template>
<NodeTemplate :node="node" :connectionType="2" @setting="settingVisible = true">
<div class="node-item">
<Icon class="circle" name="square" :color="isGray ? '#9a9a9a' : '#19AAA5'" w="60" h="60"></Icon>
<Icon class="icon" name="8" color="#fff" w="24" h="24"></Icon>
</div>
</NodeTemplate>
<!-- 配置 -->
<Config v-model="settingVisible" :node="node" v-if="settingVisible && action === 'edit'" />
<!-- 查看配置 -->
<ConfigView v-model="settingVisible" :node="node" v-if="settingVisible && action === 'view'" />
<!-- 数据生成规则 -->
<Rule v-model="settingVisible" :node="node" v-if="settingVisible && action === 'rule'" />
</template>
<script setup lang="ts">
import RuleTemplate from '../../RuleTemplate.vue'
defineProps<{ node: any }>()
</script>
<template>
<RuleTemplate :node="node" step>
<template #header-answer>答案</template>
</RuleTemplate>
</template>
...@@ -7,8 +7,9 @@ const props = defineProps<{ node: any }>() ...@@ -7,8 +7,9 @@ const props = defineProps<{ node: any }>()
const role = inject('role') as string const role = inject('role') as string
const form = reactive({ const form: any = reactive({
attr_id: '', attr_id: '',
attr_type: '',
operate: '', operate: '',
value: '' value: ''
}) })
...@@ -32,6 +33,8 @@ const operatorList = computed(() => { ...@@ -32,6 +33,8 @@ const operatorList = computed(() => {
}) })
function onAttrChange() { function onAttrChange() {
const currentUserAttr = userAttrList.value.find(item => item.id === form.attr_id)
form.attr_type = currentUserAttr ? currentUserAttr.type : ''
form.operate = '' form.operate = ''
form.value = '' form.value = ''
} }
...@@ -39,6 +42,15 @@ function onAttrChange() { ...@@ -39,6 +42,15 @@ function onAttrChange() {
function getAttr(attrId: string) { function getAttr(attrId: string) {
return userAttrList.value.find(item => item.id === attrId) return userAttrList.value.find(item => item.id === attrId)
} }
// 条件改变
function handleOperateChange(value: string) {
form.value = ''
// 区间
if (value === 'range') {
form.value = { start: undefined, end: undefined }
}
}
</script> </script>
<template> <template>
...@@ -49,18 +61,33 @@ function getAttr(attrId: string) { ...@@ -49,18 +61,33 @@ function getAttr(attrId: string) {
<el-select v-model="form.attr_id" @change="onAttrChange" placeholder="请选择属性" style="width: 130px"> <el-select v-model="form.attr_id" @change="onAttrChange" placeholder="请选择属性" style="width: 130px">
<el-option :key="item.id" v-for="item in userAttrList" :value="item.id" :label="item.name"></el-option> <el-option :key="item.id" v-for="item in userAttrList" :value="item.id" :label="item.name"></el-option>
</el-select> </el-select>
<el-select v-model="form.operate" style="margin: 0 10px; width: 130px"> <el-select v-model="form.operate" style="margin: 0 10px; width: 130px" @change="handleOperateChange">
<el-option <el-option v-for="item in operatorList" :key="item.value" :value="item.value" :label="item.label"></el-option>
v-for="item in operatorList"
:key="item.value"
:value="item.value"
:label="item.label"></el-option>
</el-select> </el-select>
<el-input <!-- 数字区间 -->
v-model="form.value" <template v-if="['2', '3'].includes(form.attr_type) && form.operate === 'range'">
placeholder="请输入" <el-input-number step-strictly :controls="false" :min="0" v-model="form.value.start" />
:maxlength="getAttr(form.attr_id)?.format" <el-input-number step-strictly :controls="false" :min="0" v-model="form.value.end" style="margin-left: 10px" />
style="width: 200px"></el-input> </template>
<!-- 日期区间 -->
<template v-else-if="form.attr_type === '4' && form.operate === 'range'">
<el-date-picker v-model="form.value.start" type="date" value-format="YYYY-MM-DD" style="width: 180px" />
<el-date-picker v-model="form.value.end" type="date" value-format="YYYY-MM-DD" style="width: 180px; margin-left: 10px" />
</template>
<!-- 时间区间 -->
<template v-else-if="form.attr_type === '5' && form.operate === 'range'">
<el-date-picker v-model="form.value.start" type="datetime" value-format="YYYY-MM-DD HH:mm:ss" style="width: 180px" />
<el-date-picker v-model="form.value.end" type="datetime" value-format="YYYY-MM-DD HH:mm:ss" style="width: 180px; margin-left: 10px" />
</template>
<template v-else-if="form.attr_type === '4' && (form.operate === 'after' || form.operate === 'before')">
<el-date-picker v-model="form.value" type="date" value-format="YYYY-MM-DD" />
</template>
<template v-else-if="form.attr_type === '5' && (form.operate === 'after' || form.operate === 'before')">
<el-date-picker v-model="form.value" type="datetime" value-format="YYYY-MM-DD HH:mm:ss" style="width: 180px; margin-left: 10px" />
</template>
<template v-else>
<el-input v-model="form.value" placeholder="请输入属性值" :maxlength="getAttr(form.attr_id)?.format" style="width: 180px"></el-input>
</template>
</div> </div>
<p style="font-size: 12px; text-align: right; color: #ccc">所选择属性满足该条件的用户,将会触发该旅程</p> <p style="font-size: 12px; text-align: right; color: #ccc">所选择属性满足该条件的用户,将会触发该旅程</p>
</div> </div>
......
<template>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="64 64 896 896">
<g>
<path
d="M384 912h496c17.7 0 32-14.3 32-32V340H384v572zm496-800H384v164h528V144c0-17.7-14.3-32-32-32zm-768 32v736c0 17.7 14.3 32 32 32h176V112H144c-17.7 0-32 14.3-32 32z"></path>
</g>
</svg>
</template>
...@@ -38,16 +38,16 @@ function handleRfmChange(rfmKey: string, item: any) { ...@@ -38,16 +38,16 @@ function handleRfmChange(rfmKey: string, item: any) {
const found = rfmResList.value.find(item => item.frm_key === rfmKey) const found = rfmResList.value.find(item => item.frm_key === rfmKey)
item.rfm_value = found?.frm_value item.rfm_value = found?.frm_value
} }
const a = [ // const a = [
{ label: '重要价值用户', label_des: '最近使用,使用频率高,消费金额大', r: '高', f: '高', m: '高', group: '组合1', guide: '留存与促活' }, // { label: '重要价值用户', label_des: '最近使用,使用频率高,消费金额大', r: '高', f: '高', m: '高', group: '组合1', guide: '留存与促活' },
{ label: '一般价值用户', label_des: '最近使用,使用频率高,消费金额小', r: '高', f: '高', m: '低', group: '组合2', guide: '放弃' }, // { label: '一般价值用户', label_des: '最近使用,使用频率高,消费金额小', r: '高', f: '高', m: '低', group: '组合2', guide: '放弃' },
{ label: '重要发展用户', label_des: '最近使用,使用频率低,消费金额大', r: '高', f: '低', m: '高', group: '组合3', guide: '拉新客户' }, // { label: '重要发展用户', label_des: '最近使用,使用频率低,消费金额大', r: '高', f: '低', m: '高', group: '组合3', guide: '拉新客户' },
{ label: '一般发展用户', label_des: '最近使用,使用频率低,消费金额小', r: '高', f: '低', m: '低', group: '组合4', guide: '放弃' }, // { label: '一般发展用户', label_des: '最近使用,使用频率低,消费金额小', r: '高', f: '低', m: '低', group: '组合4', guide: '放弃' },
{ label: '重要保持用户', label_des: '较长时间未使用,使用频率高,消费金额大', r: '低', f: '高', m: '高', group: '组合5', guide: '留存与促活' }, // { label: '重要保持用户', label_des: '较长时间未使用,使用频率高,消费金额大', r: '低', f: '高', m: '高', group: '组合5', guide: '留存与促活' },
{ label: '一般保持用户', label_des: '较长时间未使用,使用频率高,消费金额小', r: '低', f: '高', m: '低', group: '组合6', guide: '放弃' }, // { label: '一般保持用户', label_des: '较长时间未使用,使用频率高,消费金额小', r: '低', f: '高', m: '低', group: '组合6', guide: '放弃' },
{ label: '重要挽留用户', label_des: '较长时间未使用,使用频率低,消费金额大', r: '低', f: '低', m: '高', group: '组合7', guide: '流失客户' }, // { label: '重要挽留用户', label_des: '较长时间未使用,使用频率低,消费金额大', r: '低', f: '低', m: '高', group: '组合7', guide: '流失客户' },
{ label: '一般挽留用户', label_des: '较长时间未使用,使用频率低,消费金额小', r: '低', f: '低', m: '低', group: '组合8', guide: '放弃' } // { label: '一般挽留用户', label_des: '较长时间未使用,使用频率低,消费金额小', r: '低', f: '低', m: '低', group: '组合8', guide: '放弃' }
] // ]
</script> </script>
<template> <template>
...@@ -83,14 +83,14 @@ const a = [ ...@@ -83,14 +83,14 @@ const a = [
</el-option> </el-option>
</el-select> </el-select>
<el-popover popper-class="rfm-popover" placement="top" :width="800" trigger="hover"> <el-popover popper-class="rfm-popover" placement="top" :width="800" trigger="hover">
<el-table :data="a" border stripe> <el-table :data="rfmResList" border stripe>
<el-table-column prop="group" label="组合" width="70" /> <el-table-column prop="frm_extend_info.group" label="组合" width="70" />
<el-table-column prop="r" label="R值" width="52" /> <el-table-column prop="frm_extend_info.r" label="R值" width="52" />
<el-table-column prop="f" label="F值" width="52" /> <el-table-column prop="frm_extend_info.f" label="F值" width="52" />
<el-table-column prop="m" label="M值" width="52" /> <el-table-column prop="frm_extend_info.m" label="M值" width="52" />
<el-table-column prop="label" label="标签值" width="110" /> <el-table-column prop="frm_value" label="标签值" width="110" />
<el-table-column prop="label_des" label="标签说明" /> <el-table-column prop="frm_extend_info.label_desc" label="标签说明" />
<el-table-column prop="guide" label="客户营销策略" width="110" /> <el-table-column prop="frm_extend_info.customer_marketing_strategy" label="客户营销策略" width="110" />
</el-table> </el-table>
<template #reference> <template #reference>
<el-icon><QuestionFilled /></el-icon> <el-icon><QuestionFilled /></el-icon>
......
...@@ -26,7 +26,18 @@ const defaultScore = [ ...@@ -26,7 +26,18 @@ const defaultScore = [
] ]
onMounted(() => { onMounted(() => {
form.value = Object.assign({ basis: '1', rule: '101', event_id: '-1', attr_id: '', attr_type: '', config: [...defaultScore] }, form.value) form.value = Object.assign(
{
basis: '1',
rule: '101',
event_id: '-1',
attr_id: '',
attr_type: '',
config: [...defaultScore],
extend_config: { default_score_config: { switch: false, score: undefined } }
},
form.value
)
}) })
const { userAttrList, fetchUserAttrList } = useUserAttr() const { userAttrList, fetchUserAttrList } = useUserAttr()
...@@ -111,6 +122,8 @@ const a = [ ...@@ -111,6 +122,8 @@ const a = [
{ id: '004', label: '2200' }, { id: '004', label: '2200' },
{ id: '005', label: '1800' } { id: '005', label: '1800' }
] ]
const defaultOptions = Array.from({ length: 5 }).map((_, index) => ({ value: index + 1, label: index + 1 }))
</script> </script>
<template> <template>
...@@ -169,9 +182,15 @@ const a = [ ...@@ -169,9 +182,15 @@ const a = [
<el-select v-model="form.attr_id" placeholder="选择属性" style="width: 160px" @change="handleAttrChange" v-else> <el-select v-model="form.attr_id" placeholder="选择属性" style="width: 160px" @change="handleAttrChange" v-else>
<el-option v-for="item in userAttrList" :key="item.id" :label="item.name" :value="item.id"></el-option> <el-option v-for="item in userAttrList" :key="item.id" :label="item.name" :value="item.id"></el-option>
</el-select> </el-select>
<template v-if="form.basis == 1 && form.rule != '102' && form.attr_id"> <div style="flex: 1; display: flex; justify-content: space-between" v-if="form.basis == 1 && form.rule != '102' && form.attr_id">
<p>最小值:{{ userAttrRange.min }} <br />最大值:{{ userAttrRange.max }}<br />平均值:{{ userAttrRange.avg }}</p> <p>最小值:{{ userAttrRange.min }}<br />最大值:{{ userAttrRange.max }}<br />"0"值数量:{{ userAttrRange.zero_count }}</p>
</template> <p>平均值:{{ userAttrRange.avg }}<br />中位数:{{ userAttrRange.median }}<br />中位数(不含0):{{ userAttrRange.no_zero_median }}</p>
</div>
</div>
<div class="rfm-header-extra" v-if="form.rule === '101' && form.extend_config">
<p>未匹配数据默认赋值</p>
<el-select-v2 v-model="form.extend_config.default_score_config.score" :options="defaultOptions" style="width: 100px; margin: 0 10px" clearable />
<el-switch v-model="form.extend_config.default_score_config.switch"></el-switch>
</div> </div>
<div class="rfm-body"> <div class="rfm-body">
<template v-if="form.rule === '102'"> <template v-if="form.rule === '102'">
...@@ -254,4 +273,8 @@ const a = [ ...@@ -254,4 +273,8 @@ const a = [
margin: 10px 0; margin: 10px 0;
} }
} }
.rfm-header-extra {
display: flex;
align-items: center;
}
</style> </style>
...@@ -41,7 +41,14 @@ export function useMetaEvent() { ...@@ -41,7 +41,14 @@ export function useMetaEvent() {
// 最大值最小值 // 最大值最小值
export function useUserAttrRange() { export function useUserAttrRange() {
const userAttrRange = ref<{ min: string; max: string; avg: string }>({ min: '', max: '', avg: '' }) const userAttrRange = ref<{ min: string; max: string; avg: string; median: string; zero_count: string; no_zero_median: string }>({
min: '',
max: '',
avg: '',
median: '',
zero_count: '',
no_zero_median: ''
})
async function fetchUserAttrRange(member_meta_id: string) { async function fetchUserAttrRange(member_meta_id: string) {
await getMemberAttrRange({ member_meta_id }).then((res: any) => { await getMemberAttrRange({ member_meta_id }).then((res: any) => {
userAttrRange.value = res.data.detail userAttrRange.value = res.data.detail
......
<script setup> <script setup>
import { ElMessage } from 'element-plus'
import ChartCard from '@/components/ChartCard.vue' import ChartCard from '@/components/ChartCard.vue'
import { DataLine } from '@element-plus/icons-vue' import { DataLine } from '@element-plus/icons-vue'
import { useUser } from '@/composables/useAllData' import { useUser } from '@/composables/useAllData'
...@@ -8,7 +9,7 @@ import * as api from '../api' ...@@ -8,7 +9,7 @@ import * as api from '../api'
const { userValue, userList } = useUser() const { userValue, userList } = useUser()
const { eventValues, eventList } = useEvent() const { eventValues, eventList } = useEvent()
const date = ref('') const date = ref([])
const info = ref() const info = ref()
async function fetchInfo() { async function fetchInfo() {
...@@ -18,6 +19,14 @@ async function fetchInfo() { ...@@ -18,6 +19,14 @@ async function fetchInfo() {
onMounted(fetchInfo) onMounted(fetchInfo)
async function handleStart() { async function handleStart() {
if (!date.value.length) {
ElMessage.error('请选择时间区间')
return
}
if (!eventValues.value.length) {
ElMessage.error('请选择事件')
return
}
fetchEventAction() fetchEventAction()
fetchEventActionTrend() fetchEventActionTrend()
fetchEventMember() fetchEventMember()
...@@ -38,9 +47,12 @@ const eventAction = ref([]) ...@@ -38,9 +47,12 @@ const eventAction = ref([])
async function fetchEventAction() { async function fetchEventAction() {
if (!userValue.value) return if (!userValue.value) return
loading1.value = true loading1.value = true
const res = await api.getEventActionList(params.value) try {
eventAction.value = res.data.items const res = await api.getEventActionList(params.value)
loading1.value = false eventAction.value = res.data.items
} finally {
loading1.value = false
}
} }
const eventActionOption = computed(() => { const eventActionOption = computed(() => {
if (!eventAction.value.length) return if (!eventAction.value.length) return
...@@ -68,9 +80,12 @@ const eventActionTrend = ref([]) ...@@ -68,9 +80,12 @@ const eventActionTrend = ref([])
async function fetchEventActionTrend() { async function fetchEventActionTrend() {
if (!userValue.value) return if (!userValue.value) return
loading2.value = true loading2.value = true
const res = await api.getEventActionTrendList(params.value) try {
eventActionTrend.value = res.data.items const res = await api.getEventActionTrendList(params.value)
loading2.value = false eventActionTrend.value = res.data.items
} finally {
loading2.value = false
}
} }
const eventActionTrendOption = computed(() => { const eventActionTrendOption = computed(() => {
if (!eventActionTrend.value.length) return if (!eventActionTrend.value.length) return
...@@ -85,7 +100,7 @@ const eventActionTrendOption = computed(() => { ...@@ -85,7 +100,7 @@ const eventActionTrendOption = computed(() => {
}) })
const [first = {}] = eventActionTrend.value || [] const [first = {}] = eventActionTrend.value || []
return { return {
grid: { left: '5%', top: '10%', right: '5%', bottom: '5%', containLabel: true }, grid: { left: '5%', top: '10%', right: '5%', bottom: '15%', containLabel: true },
tooltip: { trigger: 'axis' }, tooltip: { trigger: 'axis' },
legend: { legend: {
bottom: '10', bottom: '10',
...@@ -107,9 +122,12 @@ const eventMember = ref([]) ...@@ -107,9 +122,12 @@ const eventMember = ref([])
async function fetchEventMember() { async function fetchEventMember() {
if (!userValue.value) return if (!userValue.value) return
loading3.value = true loading3.value = true
const res = await api.getEventMemberList(params.value) try {
eventMember.value = res.data.items const res = await api.getEventMemberList(params.value)
loading3.value = false eventMember.value = res.data.items
} finally {
loading3.value = false
}
} }
const eventMemberOption = computed(() => { const eventMemberOption = computed(() => {
if (!eventMember.value.length) return if (!eventMember.value.length) return
...@@ -142,9 +160,12 @@ const eventTime = ref([]) ...@@ -142,9 +160,12 @@ const eventTime = ref([])
async function fetchEventTime() { async function fetchEventTime() {
if (!userValue.value) return if (!userValue.value) return
loading4.value = true loading4.value = true
const res = await api.getEventTimeList(params.value) try {
eventTime.value = res.data.items const res = await api.getEventTimeList(params.value)
loading4.value = false eventTime.value = res.data.items
} finally {
loading4.value = false
}
} }
const eventTimeOption = computed(() => { const eventTimeOption = computed(() => {
if (!eventTime.value.length) return if (!eventTime.value.length) return
...@@ -158,7 +179,7 @@ const eventTimeOption = computed(() => { ...@@ -158,7 +179,7 @@ const eventTimeOption = computed(() => {
}) })
const [first = {}] = eventTime.value || [] const [first = {}] = eventTime.value || []
return { return {
grid: { left: '5%', top: '10%', right: '5%', bottom: '5%', containLabel: true }, grid: { left: '5%', top: '10%', right: '5%', bottom: '15%', containLabel: true },
tooltip: { trigger: 'axis' }, tooltip: { trigger: 'axis' },
legend: { legend: {
bottom: '10', bottom: '10',
......
...@@ -34,9 +34,12 @@ const labelHot = ref([]) ...@@ -34,9 +34,12 @@ const labelHot = ref([])
async function fetchLabelHot() { async function fetchLabelHot() {
if (!userValue.value) return if (!userValue.value) return
loading1.value = true loading1.value = true
const res = await api.getHotTags({ sso_id: userValue.value }) try {
labelHot.value = res.data.items const res = await api.getHotTags({ sso_id: userValue.value })
loading1.value = false labelHot.value = res.data.items
} finally {
loading1.value = false
}
} }
const labelHotOption = computed(() => { const labelHotOption = computed(() => {
if (!labelHot.value.length) return if (!labelHot.value.length) return
...@@ -79,9 +82,12 @@ const labelTop = ref([]) ...@@ -79,9 +82,12 @@ const labelTop = ref([])
async function fetchLabelTop() { async function fetchLabelTop() {
if (!userValue.value) return if (!userValue.value) return
loading2.value = true loading2.value = true
const res = await api.getTagTop({ sso_id: userValue.value }) try {
labelTop.value = res.data.items const res = await api.getTagTop({ sso_id: userValue.value })
loading2.value = false labelTop.value = res.data.items
} finally {
loading2.value = false
}
} }
const labelTopOption = computed(() => { const labelTopOption = computed(() => {
if (!labelTop.value.length) return if (!labelTop.value.length) return
...@@ -115,9 +121,12 @@ const labelMemberTop = ref([]) ...@@ -115,9 +121,12 @@ const labelMemberTop = ref([])
async function fetchLabelMemberTop() { async function fetchLabelMemberTop() {
if (!userValue.value) return if (!userValue.value) return
loading3.value = true loading3.value = true
const res = await api.getMemberTagTop({ sso_id: userValue.value }) try {
labelMemberTop.value = res.data.items const res = await api.getMemberTagTop({ sso_id: userValue.value })
loading3.value = false labelMemberTop.value = res.data.items
} finally {
loading3.value = false
}
} }
const labelMemberTopOption = computed(() => { const labelMemberTopOption = computed(() => {
if (!labelMemberTop.value.length) return if (!labelMemberTop.value.length) return
...@@ -149,9 +158,12 @@ const groupHot = ref([]) ...@@ -149,9 +158,12 @@ const groupHot = ref([])
async function fetchGroupHot() { async function fetchGroupHot() {
if (!userValue.value) return if (!userValue.value) return
loading4.value = true loading4.value = true
const res = await api.getHotGroups({ sso_id: userValue.value }) try {
groupHot.value = res.data.items const res = await api.getHotGroups({ sso_id: userValue.value })
loading4.value = false groupHot.value = res.data.items
} finally {
loading4.value = false
}
} }
const groupHotOption = computed(() => { const groupHotOption = computed(() => {
if (!groupHot.value.length) return if (!groupHot.value.length) return
...@@ -231,9 +243,12 @@ const groupMemberTop = ref([]) ...@@ -231,9 +243,12 @@ const groupMemberTop = ref([])
async function fetchGroupMemberTop() { async function fetchGroupMemberTop() {
if (!userValue.value) return if (!userValue.value) return
loading6.value = true loading6.value = true
const res = await api.getMemberGroupTop({ sso_id: userValue.value }) try {
groupMemberTop.value = res.data.items const res = await api.getMemberGroupTop({ sso_id: userValue.value })
loading6.value = false groupMemberTop.value = res.data.items
} finally {
loading6.value = false
}
} }
const groupMemberTopOption = computed(() => { const groupMemberTopOption = computed(() => {
if (!groupMemberTop.value.length) return if (!groupMemberTop.value.length) return
......
...@@ -38,9 +38,12 @@ async function fetchEventAction() { ...@@ -38,9 +38,12 @@ async function fetchEventAction() {
const [startDate, endDate] = eventActionDate.value const [startDate, endDate] = eventActionDate.value
if (!userValue.value || !eventValue.value || !startDate || !endDate) return if (!userValue.value || !eventValue.value || !startDate || !endDate) return
loading1.value = true loading1.value = true
const res = await api.getEventAction({ sso_id: userValue.value, start_date: startDate, end_date: endDate, event_id: eventValue.value }) try {
eventAction.value = res.data const res = await api.getEventAction({ sso_id: userValue.value, start_date: startDate, end_date: endDate, event_id: eventValue.value })
loading1.value = false eventAction.value = res.data
} finally {
loading1.value = false
}
} }
const eventActionOption = computed(() => { const eventActionOption = computed(() => {
if (!eventAction.value) return if (!eventAction.value) return
...@@ -55,7 +58,7 @@ const eventActionOption = computed(() => { ...@@ -55,7 +58,7 @@ const eventActionOption = computed(() => {
type: 'category', type: 'category',
boundaryGap: ['20%', '20%'], boundaryGap: ['20%', '20%'],
data: eventAction.value.event_action_items.map(item => { data: eventAction.value.event_action_items.map(item => {
return item.group_name + '月' return item.group_name
}) })
}, },
yAxis: { type: 'value' }, yAxis: { type: 'value' },
...@@ -87,9 +90,12 @@ async function fetchMarketing() { ...@@ -87,9 +90,12 @@ async function fetchMarketing() {
const [startDate, endDate] = marketingDate.value const [startDate, endDate] = marketingDate.value
if (!userValue.value || !startDate || !endDate) return if (!userValue.value || !startDate || !endDate) return
loading2.value = true loading2.value = true
const res = await api.getEventMarketing({ sso_id: userValue.value, start_date: startDate, end_date: endDate }) try {
marketing.value = res.data.items const res = await api.getEventMarketing({ sso_id: userValue.value, start_date: startDate, end_date: endDate })
loading2.value = false marketing.value = res.data.items
} finally {
loading2.value = false
}
} }
const marketingOption = computed(() => { const marketingOption = computed(() => {
return marketing.value.map((item, index, items) => { return marketing.value.map((item, index, items) => {
...@@ -116,9 +122,12 @@ const userEndDate = computed(() => { ...@@ -116,9 +122,12 @@ const userEndDate = computed(() => {
async function fetchUser() { async function fetchUser() {
if (!userValue.value || !userStartDate.value || !userEndDate.value) return if (!userValue.value || !userStartDate.value || !userEndDate.value) return
loading3.value = true loading3.value = true
const res = await api.getMemberList({ sso_id: userValue.value, start_date: userStartDate.value, end_date: userEndDate.value }) try {
user.value = res.data.items const res = await api.getMemberList({ sso_id: userValue.value, start_date: userStartDate.value, end_date: userEndDate.value })
loading3.value = false user.value = res.data.items
} finally {
loading3.value = false
}
} }
function format(date) { function format(date) {
...@@ -183,14 +192,14 @@ function format(date) { ...@@ -183,14 +192,14 @@ function format(date) {
<el-table-column label="当日" align="center"> <el-table-column label="当日" align="center">
<template #default="{ row }"> <template #default="{ row }">
<p>{{ row.day0 }}</p> <p>{{ row.day0 }}</p>
<p>{{ row.day0_rate }}%</p> <p class="is-red">{{ row.day0_rate }}%</p>
</template> </template>
</el-table-column> </el-table-column>
<template v-for="index in 7" :key="index"> <template v-for="index in 7" :key="index">
<el-table-column :label="index === 1 ? '次日' : '第' + index + '日'" align="center"> <el-table-column :label="index === 1 ? '次日' : '第' + index + '日'" align="center">
<template #default="{ row }"> <template #default="{ row }">
<p>{{ row['day' + index] }}</p> <p>{{ row['day' + index] }}</p>
<p>{{ row['day' + index + '_rate'] }}%</p> <p class="is-red">{{ row['day' + index + '_rate'] }}%</p>
</template> </template>
</el-table-column> </el-table-column>
</template> </template>
...@@ -313,4 +322,7 @@ function format(date) { ...@@ -313,4 +322,7 @@ function format(date) {
} }
} }
} }
.is-red {
color: var(--main-color);
}
</style> </style>
...@@ -12,11 +12,14 @@ const attrs = ref([]) ...@@ -12,11 +12,14 @@ const attrs = ref([])
async function fetchAttrs() { async function fetchAttrs() {
if (!props.ssoId || !metaAttrValue.value) return if (!props.ssoId || !metaAttrValue.value) return
loading.value = true loading.value = true
const res = await getMemberAttrs({ sso_id: props.ssoId, member_meta_id: metaAttrValue.value }) try {
attrs.value = res.data.items.map(item => { const res = await getMemberAttrs({ sso_id: props.ssoId, member_meta_id: metaAttrValue.value })
return { name: item.group_name, value: item.total } attrs.value = res.data.items.map(item => {
}) return { name: item.group_name, value: item.total }
loading.value = false })
} finally {
loading.value = false
}
} }
watchEffect(() => { watchEffect(() => {
fetchAttrs() fetchAttrs()
......
...@@ -39,10 +39,13 @@ const userTotal = ref(0) ...@@ -39,10 +39,13 @@ const userTotal = ref(0)
async function fetchGender() { async function fetchGender() {
if (!userValue.value) return if (!userValue.value) return
loading1.value = true loading1.value = true
const res = await api.getMemberGender({ sso_id: userValue.value }) try {
gender.value = res.data.items const res = await api.getMemberGender({ sso_id: userValue.value })
userTotal.value = res.data.total gender.value = res.data.items
loading1.value = false userTotal.value = res.data.total
} finally {
loading1.value = false
}
} }
const genderOption = computed(() => { const genderOption = computed(() => {
if (!gender.value.length) return if (!gender.value.length) return
...@@ -96,11 +99,14 @@ const connection = ref([]) ...@@ -96,11 +99,14 @@ const connection = ref([])
async function fetchConnections() { async function fetchConnections() {
if (!userValue.value) return if (!userValue.value) return
loading2.value = true loading2.value = true
const res = await api.getMemberConnections({ sso_id: userValue.value }) try {
connection.value = res.data.items.map(item => { const res = await api.getMemberConnections({ sso_id: userValue.value })
return { ...item, group_name: getNameByValue(item.group_name, connectionTypeList) } connection.value = res.data.items.map(item => {
}) return { ...item, group_name: getNameByValue(item.group_name, connectionTypeList) }
loading2.value = false })
} finally {
loading2.value = false
}
} }
const connectionOption = computed(() => { const connectionOption = computed(() => {
if (!connection.value.length) return if (!connection.value.length) return
...@@ -134,11 +140,14 @@ const status = ref([]) ...@@ -134,11 +140,14 @@ const status = ref([])
async function fetchStatus() { async function fetchStatus() {
if (!userValue.value) return if (!userValue.value) return
loading3.value = true loading3.value = true
const res = await api.getMemberStatus({ sso_id: userValue.value }) try {
status.value = res.data.items.map(item => { const res = await api.getMemberStatus({ sso_id: userValue.value })
return { name: getNameByValue(item.group_name, statusList), value: item.total } status.value = res.data.items.map(item => {
}) return { name: getNameByValue(item.group_name, statusList), value: item.total }
loading3.value = false })
} finally {
loading3.value = false
}
} }
const statusOption = computed(() => { const statusOption = computed(() => {
if (!status.value.length) return if (!status.value.length) return
......
...@@ -112,7 +112,7 @@ const handleStudentFollow = function () { ...@@ -112,7 +112,7 @@ const handleStudentFollow = function () {
<el-icon size="16" color="#000" @click.stop="edit"><Avatar /></el-icon> <el-icon size="16" color="#000" @click.stop="edit"><Avatar /></el-icon>
<span>自动生成事件数据</span> <span>自动生成事件数据</span>
</li> </li>
<li @click.top="viewDataProgress"> <li @click.stop="viewDataProgress">
<el-icon size="16" color="#000" @click.stop="edit"><PieChart /></el-icon> <el-icon size="16" color="#000" @click.stop="edit"><PieChart /></el-icon>
<span>数据生成进度</span> <span>数据生成进度</span>
</li> </li>
......
...@@ -11,6 +11,8 @@ const store = useMapStore() ...@@ -11,6 +11,8 @@ const store = useMapStore()
const userStore = useUserStore() const userStore = useUserStore()
const route = useRoute()
const FormDialog = defineAsyncComponent(() => import('../components/FormDialog.vue')) const FormDialog = defineAsyncComponent(() => import('../components/FormDialog.vue'))
const UserDataDialog = defineAsyncComponent(() => import('../components/UserDataDialog.vue')) const UserDataDialog = defineAsyncComponent(() => import('../components/UserDataDialog.vue'))
const EventDataDialog = defineAsyncComponent(() => import('../components/EventDataDialog.vue')) const EventDataDialog = defineAsyncComponent(() => import('../components/EventDataDialog.vue'))
...@@ -106,15 +108,19 @@ const viewDataProgress = function () { ...@@ -106,15 +108,19 @@ const viewDataProgress = function () {
let studentFollowVisible = $ref(false) let studentFollowVisible = $ref(false)
let studentFollowData = $ref<StudentFollow>() let studentFollowData = $ref<StudentFollow>()
const handleStudentFollow = function (experimentId: string, id: string, type: string) { const handleStudentFollow = function (experimentId: string, id: string, type: string) {
studentFollowVisible = true if (type === '15') {
getStudentFollow({ experiment_id: experimentId, experiment_connection_id: id }).then((res: any) => { window.open(`https://mall-h5-web.ezijing.com?id=${id}&experiment_id=${route.query.experiment_id}`)
if (res.code === 0) { } else {
const data = res.data studentFollowVisible = true
data.connect_id = id getStudentFollow({ experiment_id: experimentId, experiment_connection_id: id }).then((res: any) => {
data.type = type if (res.code === 0) {
studentFollowData = data const data = res.data
} data.connect_id = id
}) data.type = type
studentFollowData = data
}
})
}
} }
</script> </script>
...@@ -179,7 +185,7 @@ const handleStudentFollow = function (experimentId: string, id: string, type: st ...@@ -179,7 +185,7 @@ const handleStudentFollow = function (experimentId: string, id: string, type: st
margin-right: 10px; margin-right: 10px;
} }
} }
.connect-item{ .connect-item {
min-height: 166px; min-height: 166px;
} }
</style> </style>
...@@ -11,6 +11,6 @@ export function getMembersList() { ...@@ -11,6 +11,6 @@ export function getMembersList() {
} }
// 事件 // 事件
export function getEventList(params: { member_id: string }) { export function getEventList(params: { member_id: string; page?: number; 'per-page'?: number }) {
return httpRequest.get('/api/lab/v1/experiment/index/events', { params }) return httpRequest.get('/api/lab/v1/experiment/index/events', { params })
} }
...@@ -18,30 +18,37 @@ getExperimentData().then(res => { ...@@ -18,30 +18,37 @@ getExperimentData().then(res => {
}) })
// 最近活跃客户 // 最近活跃客户
let userList = $ref<{ name: string; id: string; isActive: boolean; gender: string }[]>() let userList = $ref<{ name: string; id: string; isActive: boolean; gender: string }[]>([])
getMembersList().then(res => { getMembersList().then(res => {
userList = res.data.map((element: any, index: number) => { userList = res.data.map((element: any, index: number) => {
element.isActive = index === 0 element.isActive = index === 0
return element return element
}) })
if (userList) getEvent(userList[0]?.id)
}) })
const activeUser = computed(() => {
return userList.find(item => item.isActive)
})
// 最近活跃客户的事件 // 最近活跃客户的事件
let eventData = $ref<{ list: any }>() let eventData = $ref<{ list: Array<any>; total: number }>({ list: [], total: 0 })
const getEvent = function (id: string) { const eventCurrentPage = ref(1)
const getEvent = function (id?: string) {
id = id || activeUser.value?.id
if (id) { if (id) {
getEventList({ member_id: id }).then(res => { getEventList({ member_id: id, page: eventCurrentPage.value, 'per-page': 10 }).then(res => {
eventData = res.data eventData = res.data
}) })
} }
} }
watchEffect(() => {
getEvent()
})
// 切换客户事件 // 切换客户事件
const handleUser = (item: any) => { const handleUser = (item: any) => {
userList?.map(item => (item.isActive = false && item)) userList?.map(item => (item.isActive = false && item))
item.isActive = true item.isActive = true
getEvent(item.id) eventCurrentPage.value = 1
} }
// 获取上下午 // 获取上下午
...@@ -97,27 +104,33 @@ function handleViewEvent(item: any) { ...@@ -97,27 +104,33 @@ function handleViewEvent(item: any) {
<AppCard class="card" title="最近活跃用户跟踪"> <AppCard class="card" title="最近活跃用户跟踪">
<div class="content-user"> <div class="content-user">
<div :class="item.isActive ? 'content-user_item active' : 'content-user_item'" v-for="item in userList" :key="item.id" @click="handleUser(item)"> <div :class="item.isActive ? 'content-user_item active' : 'content-user_item'" v-for="item in userList" :key="item.id" @click="handleUser(item)">
<img :src="item.gender === '1' ? 'https://webapp-pub.ezijing.com/pages/assa/dml_boy.png' : 'https://webapp-pub.ezijing.com/pages/assa/dml_girl.png'" /> <img
:src="item.gender === '1' ? 'https://webapp-pub.ezijing.com/pages/assa/dml_boy.png' : 'https://webapp-pub.ezijing.com/pages/assa/dml_girl.png'" />
<div class="name">{{ item.name }}</div> <div class="name">{{ item.name }}</div>
</div> </div>
</div> </div>
<el-empty v-if="!eventData?.list || !eventData?.list.length" description="暂无数据" :image-size="80" /> <template v-if="eventData.total">
<div class="event-box" v-for="item in eventData?.list" :key="item.id" v-else> <div class="event-box" v-for="item in eventData.list" :key="item.id">
<div class="date">{{ item.updated_time?.slice(0, item.updated_time.indexOf(' ')) }}</div> <div class="date">{{ item.updated_time?.slice(0, item.updated_time.indexOf(' ')) }}</div>
<div class="event-content"> <div class="event-content">
<div class="time"> <div class="time">
{{ item.updated_time?.slice(item.updated_time.indexOf(' '), item.updated_time.length - 3) }} {{ item.updated_time?.slice(item.updated_time.indexOf(' '), item.updated_time.length - 3) }}
{{ getDate(item.updated_time) }} {{ getDate(item.updated_time) }}
</div> </div>
<!-- <Icon :name="item.connection_type" w="30" h="30"></Icon> --> <!-- <Icon :name="item.connection_type" w="30" h="30"></Icon> -->
<div class="event"> <div class="event">
<Icon class="icon" :name="item.connection_type" :multiColor="true" w="24" h="24"></Icon> <Icon class="icon" :name="item.connection_type" :multiColor="true" w="24" h="24"></Icon>
<span>"{{ item.connection_name }}"</span> <span>"{{ item.connection_name }}"</span>
<span style="cursor: pointer" @click="handleViewEvent(item)">"{{ item.event_name }}"</span> <span style="cursor: pointer" @click="handleViewEvent(item)">"{{ item.event_name }}"</span>
</div>
</div> </div>
</div> </div>
</div> <div style="display: flex; align-items: center; justify-content: center; margin-top: 20px">
<el-pagination layout="prev, pager, next" v-model:current-page="eventCurrentPage" :total="eventData.total" hide-on-single-page />
</div>
</template>
<el-empty description="暂无数据" :image-size="80" v-else />
</AppCard> </AppCard>
</div> </div>
</div> </div>
......
...@@ -41,6 +41,11 @@ export function deleteLabel(data: { id: string }) { ...@@ -41,6 +41,11 @@ export function deleteLabel(data: { id: string }) {
return httpRequest.post('/api/lab/v1/experiment/tag/bda-delete', data) return httpRequest.post('/api/lab/v1/experiment/tag/bda-delete', data)
} }
// 删除标签
export function deleteLabels(data: { ids: string }) {
return httpRequest.post('/api/lab/v1/experiment/tag/batch-delete', data)
}
// 获取标签数据信息 // 获取标签数据信息
export function getLabelStatistics(params: { id: string }) { export function getLabelStatistics(params: { id: string }) {
return httpRequest.get('/api/lab/v1/experiment/tag/bda-statistics', { params }) return httpRequest.get('/api/lab/v1/experiment/tag/bda-statistics', { params })
......
...@@ -115,7 +115,7 @@ function handleUpdate() { ...@@ -115,7 +115,7 @@ function handleUpdate() {
</script> </script>
<template> <template>
<el-dialog title="标签规则管理" :close-on-click-modal="false" width="980px" @update:modelValue="value => $emit('update:modelValue', value)"> <el-dialog title="标签规则管理" :close-on-click-modal="false" width="1100px" @update:modelValue="value => $emit('update:modelValue', value)">
<el-form label-suffix=":" label-width="82px"> <el-form label-suffix=":" label-width="82px">
<el-row> <el-row>
<el-col :span="12"> <el-col :span="12">
......
<script setup lang="ts"> <script setup lang="ts">
import type { Label } from '../types' import type { Label } from '../types'
import { Plus } from '@element-plus/icons-vue' import { Plus, Delete } from '@element-plus/icons-vue'
import AppList from '@/components/base/AppList.vue' import AppList from '@/components/base/AppList.vue'
import LabelType from '../components/LabelType.vue' import LabelType from '../components/LabelType.vue'
import { ElMessageBox, ElMessage } from 'element-plus' import { ElMessageBox, ElMessage } from 'element-plus'
import { getLabelList, deleteLabel } from '../api' import { getLabelList, deleteLabel, deleteLabels } from '../api'
import { useMapStore } from '@/stores/map' import { useMapStore } from '@/stores/map'
import { getNameByValue, updateStatusRuleList, labelList } from '@/utils/dictionary' import { getNameByValue, updateStatusRuleList, labelList } from '@/utils/dictionary'
import { useLabelType } from '../composables/useLabelType' import { useLabelType } from '../composables/useLabelType'
...@@ -58,6 +58,7 @@ const listOptions = computed(() => { ...@@ -58,6 +58,7 @@ const listOptions = computed(() => {
{ type: 'input', prop: 'updated_operator', placeholder: '更新人', slots: 'filter-user' } { type: 'input', prop: 'updated_operator', placeholder: '更新人', slots: 'filter-user' }
], ],
columns: [ columns: [
{ type: 'selection' },
{ label: '序号', type: 'index', width: 60 }, { label: '序号', type: 'index', width: 60 },
{ label: '标签ID', prop: 'id' }, { label: '标签ID', prop: 'id' },
{ label: '标签名称', prop: 'name' }, { label: '标签名称', prop: 'name' },
...@@ -143,16 +144,35 @@ function handleSelect(id: string) { ...@@ -143,16 +144,35 @@ function handleSelect(id: string) {
appList?.refetch() appList?.refetch()
}) })
} }
let multipleSelection = $ref<Label[]>([])
function handleSelectionChange(selection: Label[]) {
multipleSelection = selection
}
const handleRemoves = async function () {
const ids = multipleSelection.map(item => item.id)
await ElMessageBox.confirm('确定要删除选中的标签数据吗?', '提示', {
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning'
})
await deleteLabels({ ids: JSON.stringify(ids) })
appList?.refetch(true)
ElMessage({ message: '删除成功', type: 'success' })
}
</script> </script>
<!-- import { useUserStore } from '@/stores/user'
const userStore = useUserStore() -->
<template> <template>
<AppCard> <AppCard>
<div class="label-wrap"> <div class="label-wrap">
<div class="label-left"><LabelType :active-id="listParams.type_id" @select="handleSelect"></LabelType></div> <div class="label-left"><LabelType :active-id="listParams.type_id" @select="handleSelect"></LabelType></div>
<AppList v-bind="listOptions" ref="appList" class="label-right"> <AppList v-bind="listOptions" ref="appList" class="label-right" @selection-change="handleSelectionChange">
<template #header-buttons> <template #header-buttons>
<el-button type="primary" :icon="Plus" @click="handleAdd" v-if="!userStore.status.tag_status">新建</el-button> <el-button type="primary" :icon="Plus" @click="handleAdd" v-if="!userStore.status.tag_status">新建</el-button>
<el-button type="primary" plain :icon="Delete" :disabled="!multipleSelection.length" @click="handleRemoves" v-permission="'experiment_tag_delete'"
>删除</el-button
>
</template> </template>
<template #filter-user> <template #filter-user>
<SelectUser v-model="listParams.updated_operator" placeholder="更新人" @change="handleRefresh"></SelectUser> <SelectUser v-model="listParams.updated_operator" placeholder="更新人" @change="handleRefresh"></SelectUser>
...@@ -161,23 +181,14 @@ const userStore = useUserStore() --> ...@@ -161,23 +181,14 @@ const userStore = useUserStore() -->
<template #table-x="{ row }"> <template #table-x="{ row }">
<el-button type="primary" plain @click="handleRule(row)">规则</el-button> <el-button type="primary" plain @click="handleRule(row)">规则</el-button>
<el-button type="primary" plain @click="handleView(row)">查看</el-button> <el-button type="primary" plain @click="handleView(row)">查看</el-button>
<el-button type="primary" plain @click="handleUpdate(row)" v-permission="'experiment_tag_update'" <el-button type="primary" plain @click="handleUpdate(row)" v-permission="'experiment_tag_update'">编辑</el-button>
>编辑</el-button <el-button type="primary" plain @click="handleRemove(row)" v-permission="'experiment_tag_delete'">删除</el-button>
>
<el-button type="primary" plain @click="handleRemove(row)" v-permission="'experiment_tag_delete'"
>删除</el-button
>
</template> </template>
</AppList> </AppList>
</div> </div>
</AppCard> </AppCard>
<!-- 新建/修改标签 --> <!-- 新建/修改标签 -->
<LabelFormDialog <LabelFormDialog v-model="formVisible" :data="currentRow" @update="handleRefresh" v-if="formVisible"></LabelFormDialog>
v-model="formVisible"
:data="currentRow"
@update="handleRefresh"
v-if="formVisible"
></LabelFormDialog>
<!-- 查看标签 --> <!-- 查看标签 -->
<LabelViewDialog v-model="viewVisible" :data="currentRow" v-if="viewVisible && currentRow"></LabelViewDialog> <LabelViewDialog v-model="viewVisible" :data="currentRow" v-if="viewVisible && currentRow"></LabelViewDialog>
<!-- 规则 --> <!-- 规则 -->
......
import httpRequest from '@/utils/axios'
// 获取实验信息
export function getExperiment() {
return httpRequest.get('/api/lab/v1/experiment/marketing-planning/experiment')
}
// 获取营销策划完成记录
export function getRecords() {
return httpRequest.get('/api/lab/v1/experiment/marketing-planning/step-complete-records')
}
// 获取连接列表
export function getConnections() {
return httpRequest.get('/api/lab/v1/experiment/marketing-planning/connections')
}
// 获取除了固定属性之外的其他用户属性
export function getMemberAttrs() {
return httpRequest.get('/api/lab/v1/experiment/marketing-planning/member-attrs')
}
// 获取事件列表
export function getEvents() {
return httpRequest.get('/api/lab/v1/experiment/marketing-planning/events')
}
// 获取当前学员步骤
export function getSteps() {
return httpRequest.get('/api/lab/v1/experiment/marketing-planning/steps')
}
// 保存步骤
export function updateStep(data: { type: string; detail: string }) {
return httpRequest.post('/api/lab/v1/experiment/marketing-planning/save-steps', data)
}
// 验证当前步骤是否已经评分
export function checkStep(data: { type: number }) {
return httpRequest.post('/api/lab/v1/experiment/marketing-planning/check-step', data)
}
// 获取用户属性数据分析
export function getMemberAttrAnalysis(params: { attr_id: string; attr_type: string }) {
return httpRequest.get('/api/lab/v1/experiment/marketing-planning/member-attr-analysis', { params })
}
// 获取用户事件数据分析
export function getMemberEventAnalysis(params: { event_id: string }) {
return httpRequest.get('/api/lab/v1/experiment/marketing-planning/member-event-analysis', { params })
}
差异被折叠。
<script setup>
import { ElMessage } from 'element-plus'
import { Plus, Minus } from '@element-plus/icons-vue'
import { useObjectiveStore } from '../stores/objective'
import { useCheckStep } from '../composables/useData'
const { isCheck } = useCheckStep(1)
const objectiveStore = useObjectiveStore()
const { addProblem, removeProblem, addObjective, removeObjective } = objectiveStore
const emit = defineEmits(['submit', 'next'])
async function handleValidate() {
const { problems, objectives } = objectiveStore
const problemLength = problems.filter(item => !item.content).length
const objectiveLength = objectives.filter(item => !item.content).length
if (problemLength || objectiveLength) {
ElMessage.error('请填写完整')
return Promise.reject()
}
}
function genFormData() {
const { problems, objectives } = objectiveStore
return { type: 1, detail: { step1: { problems, objectives } } }
}
async function handleSubmit() {
await handleValidate()
emit('submit', genFormData())
}
async function handleNext() {
await handleValidate()
emit('next', genFormData(), isCheck.value)
}
</script>
<template>
<div class="step-wrapper">
<h2 class="h2-title">营销背景及营销目标</h2>
<div class="market-step1">
<div class="market-step1-box">
<h4>当前业务面临问题/挑战</h4>
<ul>
<li v-for="(item, index) in objectiveStore.problems" :key="item.id">
<p>问题/挑战:</p>
<el-input type="textarea" v-model="item.content" show-word-limit maxlength="200" :rows="4" :disabled="isCheck"></el-input>
<el-button
type="primary"
:icon="Plus"
@click="addProblem({ content: '' })"
:disabled="isCheck"
v-if="index === objectiveStore.problems.length - 1"></el-button>
<el-button type="primary" :icon="Minus" @click="removeProblem(item.id)" :disabled="isCheck" v-else></el-button>
</li>
</ul>
</div>
<div class="market-step1-box">
<h4>业务部门营销目标</h4>
<ul>
<li v-for="(item, index) in objectiveStore.objectives" :key="item.id">
<p>营销目标:</p>
<el-input type="textarea" v-model="item.content" show-word-limit maxlength="200" :rows="4" :disabled="isCheck"></el-input>
<el-button
type="primary"
:icon="Plus"
@click="addObjective({ content: '' })"
:disabled="isCheck"
v-if="index === objectiveStore.objectives.length - 1"></el-button>
<el-button type="primary" :icon="Minus" @click="removeObjective(item.id)" :disabled="isCheck" v-else></el-button>
</li>
</ul>
</div>
</div>
<div class="market-step-footer">
<el-button @click="handleSubmit" :disabled="isCheck">保存</el-button>
<el-button type="primary" @click="handleNext">下一步</el-button>
</div>
</div>
</template>
<style lang="scss" scoped>
.market-step1 {
display: flex;
gap: 20px;
}
.market-step1-box {
flex: 1;
padding: 20px;
border-radius: 10px;
background-color: #eef2f6;
h4 {
text-align: center;
}
ul {
width: 100%;
flex: 1;
}
li {
margin-top: 20px;
display: flex;
align-items: center;
justify-content: center;
column-gap: 20px;
white-space: nowrap;
}
}
</style>
<script setup>
import { ElMessage } from 'element-plus'
import { Select } from '@element-plus/icons-vue'
import Icon from '@/components/ConnectionIcon.vue'
import { useConnectionStore } from '../stores/connection'
import { useCheckStep } from '../composables/useData'
const { isCheck } = useCheckStep(2)
const connectionStore = useConnectionStore()
const emit = defineEmits(['submit', 'next'])
function handleClick(item) {
if (isCheck.value) return
item.active = !item.active
}
async function handleValidate() {
const listLength = connectionStore.currentConnections.filter(item => item.active && !item.content).length
if (listLength) {
ElMessage.error('请填写完整')
return Promise.reject()
}
}
function genFormData() {
const { connections, activeConnections } = connectionStore
return { type: 2, detail: { step2: { connections, activeConnections } } }
}
async function handleSubmit() {
await handleValidate()
emit('submit', genFormData())
}
async function handleNext() {
await handleValidate()
emit('next', genFormData(), isCheck.value)
}
</script>
<template>
<div class="step-wrapper">
<div class="h2-title">
<h2>营销渠道选择</h2>
<el-button type="primary"><router-link :to="`/user?experiment_id=${$route.query.experiment_id}`" target="_blank">维护用户数据</router-link></el-button>
</div>
<div class="connect-list">
<div class="connect-list-item" v-for="item in connectionStore.currentConnections" :key="item.id" :class="{ 'is-active': item.active }">
<div class="connect-box connect-box__icon" @click="handleClick(item)">
<el-icon v-show="item.active"><Select /></el-icon>
<Icon w="40" h="40" :multiColor="true" class="svg" :name="item.type == 15 ? 'mall' : item.type"></Icon>
<p>{{ item.type_name }}</p>
</div>
<div class="connect-box connect-box__total">
<p>用户数据量:{{ item.member_count }}</p>
<p>用户事件数据量:{{ item.event_count }}</p>
</div>
<div v-show="item.active">
<p>*选择原因</p>
<el-input type="textarea" v-model="item.content" show-word-limit maxlength="100" :rows="3" :disabled="isCheck"></el-input>
</div>
</div>
</div>
<div class="market-step-footer">
<el-button @click="handleSubmit" :disabled="isCheck">保存</el-button>
<el-button type="primary" @click="handleNext">下一步</el-button>
</div>
</div>
</template>
<style lang="scss" scoped>
.connect-list {
display: flex;
flex-wrap: wrap;
gap: 20px;
}
.connect-list-item {
width: 300px;
line-height: 1.4;
&.is-active {
color: var(--main-color);
.connect-box__total {
color: #fff;
background-color: var(--main-color);
}
}
p {
line-height: 30px;
}
}
.connect-box {
padding: 10px;
}
.connect-box__icon {
position: relative;
height: 100px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
cursor: pointer;
.el-icon {
position: absolute;
left: 10px;
top: 10px;
}
}
.connect-box__total {
margin: 5px 0;
text-align: center;
color: var(--main-color);
}
.connect-box {
border: 1px dashed #ccc;
}
</style>
<script setup>
import { Plus, Minus } from '@element-plus/icons-vue'
import AppUpload from '@/components/base/AppUpload.vue'
import { useMemberAttrs } from '../composables/useData'
import { useMemberStore } from '../stores/member'
import { useCheckStep } from '../composables/useData'
const { isCheck } = useCheckStep(3)
const memberStore = useMemberStore()
const { memberAttrs } = useMemberAttrs()
const emit = defineEmits(['submit', 'next'])
const rules = reactive({
sex: [{ required: true, message: '请输入', trigger: 'blur' }],
source: [{ required: true, message: '请输入', trigger: 'blur' }]
})
function handleAdd() {
memberStore.addAttr({ attr_id: '', attr_content: '', attr_file: '' })
}
function handleRemove(item) {
memberStore.removeAttr(item.id)
}
const formRef = ref(null)
async function handleValidate() {
return formRef.value.validate()
}
function genFormData() {
return { type: 3, detail: { step3: { ...memberStore.member } } }
}
async function handleSubmit() {
await handleValidate()
emit('submit', genFormData())
}
async function handleNext() {
await handleValidate()
emit('next', genFormData(), isCheck.value)
}
</script>
<template>
<div>
<div class="h2-title">
<h2>用户分析</h2>
<div>
<el-button type="primary"><router-link :to="`/user?experiment_id=${$route.query.experiment_id}`" target="_blank">用户个人画像</router-link></el-button>
<el-button type="primary">
<router-link :to="`/analyze/user?experiment_id=${$route.query.experiment_id}`" target="_blank">用户整体画像</router-link>
</el-button>
</div>
</div>
<el-form label-width="150" label-suffix=":" :model="memberStore.member" :rules="rules" ref="formRef" :disabled="isCheck">
<el-form-item label="用户性别分析" prop="sex">
<div class="custom-form-item">
<div class="custom-form-item-left">
<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>
</el-form-item>
<el-form-item label="用户数据来源分析" prop="source">
<div class="custom-form-item">
<div class="custom-form-item-left">
<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>
</el-form-item>
<el-form-item label="用户属性分析">
<ul>
<li v-for="(item, index) in memberStore.member.attrs" :key="index">
<div class="custom-form-item">
<div class="custom-form-item-left">
<el-select-v2 v-model="item.attr_id" :options="memberAttrs" :props="{ label: 'name', value: 'id' }" style="width: 200px"></el-select-v2>
<el-input type="textarea" v-model="item.attr_content" show-word-limit maxlength="200" :rows="5" style="margin-top: 10px"> </el-input>
</div>
<div class="custom-form-item-right">
<AppUpload v-model="item.attr_file"></AppUpload>
<el-button type="primary" :icon="Minus" @click="handleRemove(item)"></el-button>
</div>
</div>
</li>
</ul>
<el-button type="primary" :icon="Plus" @click="handleAdd"></el-button>
</el-form-item>
</el-form>
<div class="market-step-footer">
<el-button @click="handleSubmit" :disabled="isCheck">保存</el-button>
<el-button type="primary" @click="handleNext">下一步</el-button>
</div>
</div>
</template>
<style lang="scss" scoped>
.el-card {
background-color: #eef2f6;
}
h4 {
text-align: center;
}
ul {
width: 100%;
}
li {
border-bottom: 1px solid #eef2f6;
padding-bottom: 20px;
margin-bottom: 20px;
display: flex;
align-items: center;
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>
<script setup lang="ts">
import { Plus, Minus } from '@element-plus/icons-vue'
import FormDialog from './Step4Form.vue'
import { useLabelStore } from '../stores/label'
import type { TypeState, LabelState } from '../stores/label'
import { useCheckStep } from '../composables/useData'
const { isCheck } = useCheckStep(4)
const labelStore = useLabelStore()
const emit = defineEmits(['submit', 'next'])
const dialogVisible = ref(false)
const currentRow = ref()
// 添加
function handleAdd(item: TypeState) {
currentRow.value = { type_id: item.id, type_name: item.name }
dialogVisible.value = true
}
// 查看
function handleView(item: LabelState) {
currentRow.value = item
dialogVisible.value = true
}
// 删除
function handleRemove(item: LabelState) {
labelStore.removeLabel(item.id)
}
function genFormData() {
const { types, labels, treeLabels } = labelStore
return { type: 4, detail: { step4: { types, labels, treeLabels } } }
}
async function handleSubmit() {
emit('submit', genFormData())
}
async function handleNext() {
emit('next', genFormData(), isCheck.value)
}
</script>
<template>
<div>
<h2 class="h2-title">标签体系设计</h2>
<div class="market-label">
<div class="market-label-box" v-for="item in labelStore.treeLabels" :key="item.name">
<h4>{{ item.name }}</h4>
<ul v-if="item.children?.length">
<li v-for="child in item.children" :key="child.id">
<p @click="handleView(child)">{{ child.name }}</p>
<!-- <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>
</li>
</ul>
<el-empty desc="暂无数据" v-else></el-empty>
<el-button type="primary" :icon="Plus" @click="handleAdd(item)" :disabled="isCheck"></el-button>
</div>
</div>
<div class="market-step-footer">
<el-button @click="handleSubmit" :disabled="isCheck">保存</el-button>
<el-button type="primary" @click="handleNext">下一步</el-button>
</div>
<FormDialog v-model="dialogVisible" :data="currentRow" v-if="dialogVisible" />
</div>
</template>
<style lang="scss" scoped>
.market-label {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 20px;
}
.market-label-box {
padding: 20px;
border-radius: 20px;
background-color: #eef2f6;
min-height: 400px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
ul {
width: 100%;
flex: 1;
}
li {
margin: 20px 0;
display: flex;
align-items: center;
justify-content: center;
column-gap: 10px;
cursor: pointer;
p {
flex: 1;
line-height: 40px;
text-align: center;
background-color: #fff;
border-radius: 10px;
border: 1px solid #bbb;
}
}
}
</style>
<script setup>
import { nanoid } from 'nanoid'
import Step4FormAttr from './Step4FormAttr.vue'
import Step4FormEvent from './Step4FormEvent.vue'
import { useMemberAttrs, useEvents } from '../composables/useData'
import { useLabelStore } from '../stores/label'
import { useCheckStep } from '../composables/useData'
const { isCheck } = useCheckStep(4)
const labelStore = useLabelStore()
const { addLabel, updateLabel } = labelStore
const emit = defineEmits(['update:modelValue'])
const props = defineProps(['data'])
const form = reactive({
id: nanoid(4),
type_id: '',
type_name: '',
name: '',
data_type: '用户属性',
attr_id: '',
attr_name: '',
attr_type: '',
event_id: '',
event_name: '',
desc: ''
})
watchEffect(() => {
Object.assign(form, props.data)
})
const formRef = ref(null)
const rules = reactive({
name: [{ required: true, message: '请输入', trigger: 'blur' }],
desc: [{ required: true, message: '请输入', trigger: 'blur' }]
})
const { memberAttrs } = useMemberAttrs()
const handleAttrChange = value => {
const found = memberAttrs.value.find(item => item.id === value)
form.attr_name = found.name
form.attr_type = found.type
form.attr_type_name = found.type_name
}
const { events } = useEvents()
const handleEventChange = value => {
const found = events.value.find(item => item.id === value)
form.event_name = found.name
}
const handleSubmit = async () => {
await formRef.value.validate()
props.data.id ? updateLabel(toRaw(form)) : addLabel(toRaw(form))
emit('update:modelValue', false)
}
</script>
<template>
<el-dialog title="设计标签" width="800" @closed="$emit('update:modelValue', false)">
<el-form label-suffix=":" label-width="160" :model="form" :rules="rules" ref="formRef" :disabled="isCheck">
<el-form-item label="标签所属目录">
<el-select-v2 v-model="form.type_name" :options="labelStore.types" :props="{ label: 'name', value: 'name' }" disabled></el-select-v2>
</el-form-item>
<el-form-item label="标签名称" prop="name">
<el-input v-model="form.name" />
</el-form-item>
<el-form-item label="关联数据类型" prop="data_type">
<el-radio-group v-model="form.data_type">
<el-radio label="用户属性" value="用户属性"></el-radio>
<el-radio label="用户行为事件" value="用户行为事件"></el-radio>
<el-radio label="其他" value="其他"></el-radio>
</el-radio-group>
</el-form-item>
<!-- 用户属性 -->
<template v-if="form.data_type == '用户属性'">
<el-form-item label="用户属性字段">
<el-select-v2 v-model="form.attr_id" :options="memberAttrs" :props="{ label: 'name', value: 'id' }" @change="handleAttrChange"></el-select-v2>
</el-form-item>
<el-form-item label="字段类型">{{ form.attr_type_name }}</el-form-item>
<Step4FormAttr :attrId="form.attr_id" :attrType="form.attr_type"></Step4FormAttr>
</template>
<!-- 用户行为事件 -->
<template v-if="form.data_type == '用户行为事件'">
<el-form-item label="用户行为事件">
<el-select-v2 v-model="form.event_id" :options="events" :props="{ label: 'name', value: 'id' }" @change="handleEventChange"></el-select-v2>
</el-form-item>
<Step4FormEvent :eventId="form.event_id"></Step4FormEvent>
</template>
<el-form-item label="标签设置规则及说明" prop="desc">
<el-input type="textarea" v-model="form.desc" show-word-limit maxlength="100" :rows="4"></el-input>
</el-form-item>
</el-form>
<template #footer>
<el-row justify="center">
<el-button plain auto-insert-space @click="$emit('update:modelValue', false)">关闭</el-button>
<el-button type="primary" auto-insert-space @click="handleSubmit" :disabled="isCheck">保存</el-button>
</el-row>
</template>
</el-dialog>
</template>
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论