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

feat: 新增运营策略

上级 0fa22548
......@@ -161,3 +161,8 @@ export function getProductList(params?: {
export function getAuth() {
return httpRequest.get('/api/lab/v1/experiment/auth/all')
}
// 获取实验旅程中的群组列表
export function getGroupList() {
return httpRequest.get('/api/lab/v1/experiment/itinerary/groups')
}
<script setup lang="ts">
import type { GroupRule } from '@/types'
import { PriceTag, Plus, CloseBold } from '@element-plus/icons-vue'
import { useGroup } from '@/composables/useAllData'
const rule = defineModel<GroupRule>({ default: { current_logic_operate: 'and', items: [] } })
const { buttonText = '添加条件' } = defineProps<{
buttonText?: string
}>()
const { groupList } = useGroup()
// 获取逻辑运算符名称
function getLogicalName(value: 'and' | 'or') {
return value === 'or' ? '或' : '且'
}
// 切换逻辑运算符
function toggleOperate(rule: GroupRule) {
rule.current_logic_operate = rule.current_logic_operate === 'or' ? 'and' : 'or'
}
// 添加条件
function handleAdd(items: any[]) {
items.push({ tag_id: '' })
}
// 删除
function handleRemove(items: any[], index: number) {
items.splice(index, 1)
}
</script>
<template>
<el-card shadow="never">
<template #header>
<slot name="header">
<el-button circle color="#567722" :icon="PriceTag"></el-button>
群组满足以下条件
</slot>
</template>
<div class="rule" v-if="rule.items.length">
<div class="rule-operator">
<span @click="toggleOperate(rule)">{{ getLogicalName(rule.current_logic_operate) }}</span>
</div>
<div class="rule-list">
<el-row justify="space-between" class="rule-item" v-for="(item, index) in rule.items" :key="index">
<div>
群组 等于
<el-form-item>
<el-select v-model="item.tag_id" style="width: 300px">
<el-option
v-for="option in groupList"
:key="option.id"
:label="option.name"
:value="option.id"></el-option>
</el-select>
</el-form-item>
</div>
<el-button text :icon="CloseBold" @click="handleRemove(rule.items, index)"></el-button>
</el-row>
</div>
</div>
<el-button text :icon="Plus" @click="handleAdd(rule.items)">{{ buttonText }}</el-button>
</el-card>
</template>
<style src="@/assets/styles/rule.scss"></style>
......@@ -7,6 +7,10 @@ import { useRfmRes } from '@/composables/useRFMData'
// const tagRule = ref(inject('tagRule') as TagRule)
const tagRule = defineModel<TagRule>({ default: { current_logic_operate: 'and', items: [] } })
const { buttonText = '添加条件' } = defineProps<{
buttonText?: string
}>()
const { tagList } = useTag()
const { rfmResList } = useRfmRes()
......@@ -31,11 +35,11 @@ function handleRemove(items: any[], index: number) {
}
function showRfm(id: string) {
return tagList.value.find(item => item.id === id)?.label == '4'
return tagList.value.find((item) => item.id === id)?.label == '4'
}
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
}
// const a = [
......@@ -53,8 +57,10 @@ function handleRfmChange(rfmKey: string, item: any) {
<template>
<el-card shadow="never">
<template #header>
<el-button circle color="#567722" :icon="PriceTag"></el-button>
标签满足以下条件
<slot name="header">
<el-button circle color="#567722" :icon="PriceTag"></el-button>
标签满足以下条件
</slot>
</template>
<div class="rule" v-if="tagRule.items.length">
<div class="rule-operator">
......@@ -66,11 +72,23 @@ function handleRfmChange(rfmKey: string, item: any) {
标签 等于
<el-form-item>
<el-select v-model="item.tag_id" style="width: 300px">
<el-option v-for="option in tagList" :key="option.id" :label="option.name" :value="option.id"></el-option>
<el-option
v-for="option in tagList"
:key="option.id"
:label="option.name"
:value="option.id"></el-option>
</el-select>
<template v-if="showRfm(item.tag_id)">
<el-select v-model="item.rfm_key" @change="value => handleRfmChange(value, item)" style="width: 400px; margin: 0 10px">
<el-option v-for="item in rfmResList" :key="item.frm_key" :label="item.frm_value" :value="item.frm_key" style="height: auto">
<el-select
v-model="item.rfm_key"
@change="(value) => handleRfmChange(value, item)"
style="width: 400px; margin: 0 10px">
<el-option
v-for="item in rfmResList"
:key="item.frm_key"
:label="item.frm_value"
:value="item.frm_key"
style="height: auto">
<div style="line-height: 24px; padding: 5px 0">
<p>
<span style="float: left">{{ item.frm_value }}</span>
......@@ -78,7 +96,9 @@ function handleRfmChange(rfmKey: string, item: any) {
{{ item.frm_extend_info.customer_marketing_strategy }}
</span>
</p>
<p style="clear: both; color: var(--el-text-color-secondary); font-size: 13px">{{ item.frm_extend_info.label_desc }}</p>
<p style="clear: both; color: var(--el-text-color-secondary); font-size: 13px">
{{ item.frm_extend_info.label_desc }}
</p>
</div>
</el-option>
</el-select>
......@@ -90,7 +110,10 @@ function handleRfmChange(rfmKey: string, item: any) {
<el-table-column prop="frm_extend_info.m" label="M值" width="52" />
<el-table-column prop="frm_value" label="标签值" width="110" />
<el-table-column prop="frm_extend_info.label_desc" label="标签说明" />
<el-table-column prop="frm_extend_info.customer_marketing_strategy" label="客户营销策略" width="110" />
<el-table-column
prop="frm_extend_info.customer_marketing_strategy"
label="客户营销策略"
width="110" />
</el-table>
<template #reference>
<el-icon><QuestionFilled /></el-icon>
......@@ -103,7 +126,7 @@ function handleRfmChange(rfmKey: string, item: any) {
</el-row>
</div>
</div>
<el-button text :icon="Plus" @click="handleAdd(tagRule.items)">添加条件</el-button>
<el-button text :icon="Plus" @click="handleAdd(tagRule.items)">{{ buttonText }}</el-button>
</el-card>
</template>
......
import { getMetaUserAttrList, getMetaEventList, getTagList, getConnectionList, getUserList } from '@/api/base'
import {
getMetaUserAttrList,
getMetaEventList,
getTagList,
getConnectionList,
getUserList,
getGroupList,
} from '@/api/base'
import { useMapStore } from '@/stores/map'
// 用户属性类型
......@@ -36,6 +43,12 @@ export interface ConnectionType {
config_attributes: any
}
// 群组类型
export interface GroupType {
id: string
name: string
}
// 所有用户属性
const userAttrList = ref<AttrType[]>([])
const userAttrLoading = ref(false)
......@@ -100,8 +113,9 @@ export function useConnection() {
const connectionType = useMapStore().getMapValuesByKey('experiment_connection_type')
await getConnectionList().then((res: any) => {
connectionList.value = res.data.items.map((item: any) => {
const connection = connectionType.find(type => type.value == item.type)
const attrs = typeof item.config_attributes === 'string' ? JSON.parse(item.config_attributes) : item.config_attributes
const connection = connectionType.find((type) => type.value == item.type)
const attrs =
typeof item.config_attributes === 'string' ? JSON.parse(item.config_attributes) : item.config_attributes
const name = Array.isArray(attrs) ? attrs.find((item: any) => item.prop === 'name')?.value : attrs.name
return { ...item, config_attributes: attrs, name, type_name: connection?.label || item.type }
})
......@@ -142,3 +156,21 @@ export function useUser() {
})
return { fetchUserList, userList, userValue }
}
// 所有群组
const groupList = ref<GroupType[]>([])
export function useGroup() {
function fetchGroupList() {
getGroupList().then((res: any) => {
groupList.value = res.data.items
})
}
onMounted(() => {
if (!groupList.value?.length) fetchGroupList()
})
function getGroup(groupId: string) {
return groupList.value.find((item) => item.id === groupId)
}
return { fetchGroupList, groupList, getGroup }
}
......@@ -93,7 +93,7 @@ watchEffect(() => fetchInfo())
const rules = ref<FormRules>({
name: [{ required: true, message: '请输入群组名称' }],
teacher_id:[{ required: true, message: '请选择老师标签' }],
teacher_id: [{ required: true, message: '请选择老师标签' }],
})
const questionType = props.data.type === '1' ? '301' : '302'
......
import httpRequest from '@/utils/axios'
import type { StrategyListRequest, StrategyCreateRequest, StrategyUpdateRequest } from './types'
// 获取策略列表
export function getStrategyList(params?: StrategyListRequest) {
return httpRequest.get('/api/lab/v1/experiment/operational-strategies/list', { params })
}
// 创建策略
export function createStrategy(data: StrategyCreateRequest) {
return httpRequest.post('/api/lab/v1/experiment/operational-strategies/create', data)
}
// 更新策略
export function updateStrategy(data: StrategyUpdateRequest) {
return httpRequest.post('/api/lab/v1/experiment/operational-strategies/update', data)
}
// 删除策略
export function deleteStrategy(data: { id: string }) {
return httpRequest.post('/api/lab/v1/experiment/operational-strategies/delete', data)
}
// 获取策略详情
export function getStrategyInfo(params: { id: string }) {
return httpRequest.get('/api/lab/v1/experiment/operational-strategies/view', { params })
}
差异被折叠。
<script setup lang="ts">
import type { Strategy } from '../types'
import type { FormInstance } from 'element-plus'
import { updateStatusRuleList, dateUnitList, weekList } from '@/utils/dictionary'
import { getStrategyInfo } from '../api'
import { useMapStore } from '@/stores/map'
import LabelRule from '@/components/rule/LabelRule.vue'
import GroupRule from '@/components/rule/GroupRule.vue'
import { useConnection } from '@/composables/useAllData'
const props = defineProps<{
data?: Strategy
}>()
defineEmits<{
(e: 'update'): void
(e: 'update:modelValue', visible: boolean): void
}>()
const typeList = useMapStore().getMapValuesByKey('strategy_type')
const industryList = useMapStore().getMapValuesByKey('strategy_industry')
const isUpdate = $computed(() => !!props.data?.id)
const title = $computed(() => (isUpdate ? '修改运营策略' : '新建运营策略'))
const { connectionList } = useConnection()
const formRef = $ref<FormInstance>()
const form: any = reactive({
id: '',
name: '',
type: '',
industry: '',
update_status: '2',
update_rule: { type: 1, info: 1 },
status: '1',
target: '',
scene: '',
tags_id: { current_logic_operate: 'and', items: [] },
groups_id: { current_logic_operate: 'and', items: [] },
time: [],
connections_id: '',
})
function fetchInfo() {
if (!props.data?.id) return
getStrategyInfo({ id: props.data.id }).then(({ data }) => {
const tagRule = data.tags_id ? JSON.parse(data.tags_id) : { current_logic_operate: 'and', items: [] }
const groupRule = data.groups_id ? JSON.parse(data.groups_id) : { current_logic_operate: 'and', items: [] }
Object.assign(form, data, {
update_rule: { type: 1, info: 1 },
tags_id: tagRule,
groups_id: groupRule,
time: data.time ? data.time.split(',') : [],
})
})
}
watchEffect(() => fetchInfo())
</script>
<template>
<el-dialog :title="title" :close-on-click-modal="false" width="980px" @closed="$emit('update:modelValue', false)">
<el-form ref="formRef" :model="form" label-suffix=":" label-width="94px" disabled>
<el-form-item label="策略名称" prop="name">
<el-input v-model="form.name" placeholder="请输入" />
</el-form-item>
<el-form-item label="行业" prop="industry">
<el-select v-model="form.industry" style="width: 100%" :disabled="isUpdate">
<el-option v-for="item in industryList" :key="item.value" :label="item.label" :value="item.value"></el-option>
</el-select>
</el-form-item>
<el-form-item label="策略类型" prop="type">
<el-select v-model="form.type" style="width: 100%" :disabled="isUpdate">
<el-option v-for="item in typeList" :key="item.value" :label="item.label" :value="item.value"></el-option>
</el-select>
</el-form-item>
<el-form-item label="更新频率" prop="update_status">
<el-radio-group v-model="form.update_status">
<el-radio
v-for="item in updateStatusRuleList"
:key="item.value"
:value="item.value"
:disabled="item.value === '1'">
{{ item.label }}
</el-radio>
</el-radio-group>
<div class="update-rule-wrap" v-if="form.update_status === '1'">
<span></span>
<el-select v-model="form.update_rule.type" placeholder=" " style="width: 60px">
<el-option
v-for="item in dateUnitList"
:key="item.value"
:label="item.label"
:value="item.value"></el-option>
</el-select>
<template v-if="form.update_rule.type === 1">
<span>的凌晨更新</span>
</template>
<template v-if="form.update_rule.type === 2">
<span></span>
<el-select v-model="form.update_rule.info" placeholder=" " style="width: 80px">
<el-option v-for="item in weekList" :key="item.value" :label="item.label" :value="item.value"></el-option>
</el-select>
<span>的凌晨更新</span>
</template>
<template v-if="form.update_rule.type === 3">
<span></span>
<el-select v-model="form.update_rule.info" placeholder=" " style="width: 60px">
<el-option v-for="item in 6" :key="item" :label="item" :value="item"></el-option>
</el-select>
<span>天的凌晨更新</span>
</template>
</div>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-switch v-model="form.status" active-text="生效" active-value="1" inactive-text="失效" inactive-value="0" />
</el-form-item>
<template v-if="isUpdate">
<el-tag style="margin: 20px 0">目标与场景</el-tag>
<el-card shadow="never">
<template #header>输入策略目标</template>
<el-form-item prop="target" label-width="auto">
<el-input
type="textarea"
v-model="form.target"
:rows="6"
placeholder="例如:提升新用户注册率20%,提升月活跃用户数15%,提升付费转化率5%等" />
</el-form-item>
</el-card>
<el-card shadow="never" style="margin-top: 20px">
<template #header>输入策略场景</template>
<el-form-item prop="scene" label-width="auto">
<el-input
type="textarea"
v-model="form.scene"
:rows="6"
placeholder="例如:新用户首次访问APP第N日,用户连续N天未登录,用户将商品加入购物车但是3小时未支付。" />
</el-form-item>
</el-card>
<el-tag style="margin: 20px 0">目标人群</el-tag>
<LabelRule v-model="form.tags_id" buttonText="添加标签">
<template #header>请选择标签</template>
</LabelRule>
<GroupRule v-model="form.groups_id" buttonText="添加群组" style="margin-top: 20px">
<template #header>请选择群组</template>
</GroupRule>
<el-tag style="margin: 20px 0">运营计划</el-tag>
<el-form-item label="运营时间" prop="time">
<el-date-picker v-model="form.time" type="datetimerange" value-format="YYYY-MM-DD HH:mm:ss" />
</el-form-item>
<el-form-item label="运营渠道" prop="connections_id">
<el-select v-model="form.connections_id">
<el-option v-for="item in connectionList" :key="item.id" :label="item.name" :value="item.id"></el-option>
</el-select>
</el-form-item>
</template>
</el-form>
<template #footer>
<el-row justify="center">
<el-button plain auto-insert-space @click="$emit('update:modelValue', false)">关闭</el-button>
</el-row>
</template>
</el-dialog>
</template>
<style lang="scss">
.update-rule-wrap {
width: 100%;
.el-select {
margin: 0 10px;
}
}
</style>
import type { RouteRecordRaw } from 'vue-router'
import Layout from '@/components/layout/Index.vue'
const routes: RouteRecordRaw[] = [
{
path: '/strategy',
component: Layout,
children: [{ path: '', component: () => import('./views/Index.vue') }]
}
]
export { routes }
import type { Operator } from '@/types'
// 策略
export interface Strategy {
id: string
name: string
type: string
industry: string
status: string
update_status: string // '1' | '2'
update_rule: string
created_time: string
created_operator: Operator
updated_time: string
updated_operator: Operator
}
// 策略更新规则
export interface StrategyUpdateRule {
type: 1 | 2 | 3
info: number
}
export type StrategyListRequest = Pick<Strategy, 'id' | 'name'> & { experiment_id?: string }
export type StrategyUpdateRequest = Pick<
Strategy,
'id' | 'name' | 'type' | 'industry' | 'status' | 'update_status' | 'update_rule'
> & {
experiment_id?: string
}
export type StrategyCreateRequest = Omit<StrategyUpdateRequest, 'id'>
<script setup lang="ts">
import type { Strategy } from '../types'
import { Plus, Delete } from '@element-plus/icons-vue'
import AppList from '@/components/base/AppList.vue'
import { ElMessageBox, ElMessage } from 'element-plus'
import { getStrategyList, deleteStrategy } from '../api'
import { useMapStore } from '@/stores/map'
import { getNameByValue, updateStatusRuleList } from '@/utils/dictionary'
import SelectUser from '@/components/SelectUser.vue'
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
const FormDialog = defineAsyncComponent(() => import('../components/FormDialog.vue'))
const ViewDialog = defineAsyncComponent(() => import('../components/ViewDialog.vue'))
const typeList = useMapStore().getMapValuesByKey('strategy_type')
const industryList = useMapStore().getMapValuesByKey('strategy_industry')
const statusList = useMapStore().getMapValuesByKey('system_status')
const appList = $ref<InstanceType<typeof AppList> | null>(null)
// 列表配置
const listParams = reactive({ name: '', type_id: '', status: '', updated_operator: '' })
const listOptions = computed(() => {
return {
remote: {
httpRequest: getStrategyList,
params: listParams,
beforeRequest(params: any, isReset: boolean) {
if (isReset) {
listParams.updated_operator = ''
listParams.type_id = ''
} else {
params.updated_operator = listParams.updated_operator
}
return params
},
},
filters: [
{ type: 'input', prop: 'name', placeholder: '请输入策略名称' },
{
type: 'select',
prop: 'industry',
placeholder: '请选择策略行业',
options: industryList,
},
{
type: 'select',
prop: 'type',
placeholder: '请选择策略类型',
options: typeList,
},
{ type: 'select', prop: 'status', placeholder: '请选择策略状态', options: statusList },
{ type: 'input', prop: 'updated_operator', placeholder: '更新人', slots: 'filter-user' },
],
columns: [
{ type: 'selection' },
{ label: '序号', type: 'index', width: 60 },
{ label: '策略ID', prop: 'id' },
{ label: '策略名称', prop: 'name' },
{
label: '策略类型',
prop: 'type',
computed({ row }: { row: Strategy }) {
return getNameByValue(row.type, typeList)
},
},
{
label: '策略行业',
prop: 'industry',
computed({ row }: { row: Strategy }) {
return getNameByValue(row.industry, industryList)
},
},
{
label: '更新频率',
prop: 'update_status',
computed({ row }: { row: Strategy }) {
return getNameByValue(row.update_status, updateStatusRuleList)
},
},
{
label: '状态',
prop: 'status',
computed({ row }: { row: Strategy }) {
const color = row.status === '1' ? 'var(--main-success-color)' : 'var(--main-color)'
return `<span style="color: ${color}">${getNameByValue(row.status, statusList)}</span>`
},
},
{
label: '更新人',
prop: 'updated_operator_name',
},
{ label: '更新时间', prop: 'updated_time' },
{ label: '操作', slots: 'table-x', width: 320 },
],
}
})
// 刷新
function handleRefresh() {
appList?.refetch()
}
let formVisible = $ref(false)
let currentRow = $ref<Strategy>()
// 新建
function handleAdd() {
currentRow = undefined
formVisible = true
}
// 修改
function handleUpdate(row: Strategy) {
currentRow = row
formVisible = true
}
// 删除
function handleRemove(row: Strategy) {
ElMessageBox.confirm('确定要删除该策略吗?', '提示').then(() => {
deleteStrategy({ id: row.id }).then(() => {
ElMessage({ message: '删除成功', type: 'success' })
handleRefresh()
})
})
}
// 查看
let viewVisible = $ref(false)
function handleView(row: Strategy) {
currentRow = row
viewVisible = true
}
let multipleSelection = $ref<Strategy[]>([])
function handleSelectionChange(selection: Strategy[]) {
multipleSelection = selection
}
const handleRemoves = async function () {
const ids = multipleSelection.map((item) => item.id)
await ElMessageBox.confirm('确定要删除选中的策略数据吗?', '提示', {
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning',
})
await deleteStrategy({ id: ids.join(',') })
appList?.refetch(true)
ElMessage({ message: '删除成功', type: 'success' })
}
</script>
<template>
<AppCard>
<AppList v-bind="listOptions" ref="appList" class="label-right" @selection-change="handleSelectionChange">
<template #header-buttons>
<div style="display: flex; justify-content: space-between">
<div>
<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
>
</div>
</div>
</template>
<template #filter-user>
<SelectUser v-model="listParams.updated_operator" placeholder="更新人" @change="handleRefresh"></SelectUser>
</template>
<template #table-x="{ row }">
<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
>
<el-button type="primary" plain @click="handleRemove(row)" v-permission="'experiment_tag_delete'"
>删除</el-button
>
</template>
</AppList>
</AppCard>
<!-- 新建/修改策略 -->
<FormDialog v-model="formVisible" :data="currentRow" @update="handleRefresh" v-if="formVisible"></FormDialog>
<!-- 查看策略 -->
<ViewDialog v-model="viewVisible" :data="currentRow" v-if="viewVisible && currentRow"></ViewDialog>
</template>
......@@ -253,6 +253,13 @@ const adminMenus: IMenuItem[] = [
icon: markRaw(IconGroup),
// tag: 'experiment_groups',
},
{
id: 9,
name: '运营策略管理',
path: '/strategy',
icon: markRaw(IconLiveTalk),
// tag: 'experiment_groups',
},
],
},
{
......
......@@ -143,3 +143,9 @@ export interface MaterialProp {
status: string
isView: boolean
}
// 群组规则
export interface GroupRule {
current_logic_operate: 'and' | 'or'
items: any[]
}
......@@ -50,11 +50,11 @@ export default defineConfig(({ mode }) => ({
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api\/qianfan/, ''),
},
// '/api/resource': {
// target: 'http://com-resource-admin-test.ezijing.com/',
// changeOrigin: true,
// rewrite: path => path.replace(/^\/api\/resource/, '')
// },
'/api/lab': {
target: 'http://local-com-resource-api.frontend.ezijing.com',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api\/lab/, ''),
},
'/api': 'https://saas-dml.ezijing.com',
},
},
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论