提交 6dfae3f3 authored 作者: lhh's avatar lhh
......@@ -15,6 +15,7 @@
"@vueuse/core": "^10.3.0",
"axios": "^1.5.0",
"blueimp-md5": "^2.19.0",
"dayjs": "^1.11.10",
"echarts": "^5.4.3",
"echarts-wordcloud": "^2.1.0",
"element-plus": "^2.3.14",
......@@ -2593,9 +2594,9 @@
}
},
"node_modules/dayjs": {
"version": "1.11.7",
"resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.7.tgz",
"integrity": "sha512-+Yw9U6YO5TQohxLcIkrXBeY73WP3ejHWVvx8XCk3gxvQDCTEmS48ZrSZCKciI7Bhl/uCMyxYtE9UqRILmFphkQ=="
"version": "1.11.10",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz",
"integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ=="
},
"node_modules/de-indent": {
"version": "1.0.2",
......
......@@ -22,6 +22,7 @@
"@vueuse/core": "^10.3.0",
"axios": "^1.5.0",
"blueimp-md5": "^2.19.0",
"dayjs": "^1.11.10",
"echarts": "^5.4.3",
"echarts-wordcloud": "^2.1.0",
"element-plus": "^2.3.14",
......
......@@ -106,6 +106,17 @@ export function getMemberEvents() {
}
// 用户属性搜索
export function searchEventAttrs(params?: { search: string; experiment_meta_event_id?: string; experiment_meta_event_attr_id?: string; page?: number; per_page?: number }) {
export function searchEventAttrs(params?: {
search: string
experiment_meta_event_id?: string
experiment_meta_event_attr_id?: string
page?: number
per_page?: number
}) {
return httpRequest.get('/api/lab/v1/experiment/event/search-attributes', { params })
}
// 获取当前实验下可选的人员列表
export function getUserList() {
return httpRequest.get('/api/lab/v1/experiment/analyse/users')
}
<script setup lang="ts">
import { useFullscreen } from '@vueuse/core'
import { FullScreen } from '@element-plus/icons-vue'
import { use } from 'echarts/core'
import { CanvasRenderer } from 'echarts/renderers'
import { BarChart, PieChart, LineChart, PictorialBarChart, FunnelChart, RadarChart } from 'echarts/charts'
import { TitleComponent, TooltipComponent, LegendComponent, GridComponent } from 'echarts/components'
import VChart from 'vue-echarts'
import 'echarts-wordcloud'
use([
CanvasRenderer,
BarChart,
PieChart,
LineChart,
PictorialBarChart,
FunnelChart,
RadarChart,
TitleComponent,
TooltipComponent,
LegendComponent,
GridComponent
])
const props = defineProps<{ title?: string; options?: any; loading?: boolean }>()
const el = ref<HTMLElement | null>(null)
const { isFullscreen, toggle } = useFullscreen(el)
const isEmpty = computed(() => {
if (!props.options) return true
return !Object.keys(props.options)
})
</script>
<template>
<div class="chart-card" ref="el" :class="{ isFullscreen }">
<div class="chart-hd">
<slot name="title">
<h3>{{ props.title }}</h3>
</slot>
<div class="tools">
<slot name="tools"></slot>
<el-tooltip effect="dark" :content="isFullscreen ? '退出全屏' : '全屏'">
<el-icon class="icon-fullscreen" @click="toggle"><FullScreen /></el-icon>
</el-tooltip>
</div>
</div>
<div class="chart-bd" v-loading="loading">
<slot>
<el-empty v-if="isEmpty" />
<v-chart class="chart" :option="options" autoresize ref="chart" v-else />
</slot>
</div>
</div>
</template>
<style lang="scss">
.chart-card {
display: flex;
flex-direction: column;
flex: 1;
background-color: rgba(234, 234, 234, 0.6);
border-radius: 6px;
overflow: hidden;
&.isFullscreen {
.chart {
height: 100%;
}
}
}
.chart-hd {
display: flex;
align-items: center;
justify-content: space-between;
position: relative;
padding: 10px 20px;
height: 30px;
color: rgba(0, 0, 0, 0.8);
h3 {
font-weight: bold;
font-size: 16px;
line-height: 30px;
}
.tools {
display: flex;
align-items: center;
}
.icon-fullscreen {
margin-left: 10px;
font-size: 16px;
cursor: pointer;
}
}
.chart-bd {
flex: 1;
background-color: #fff;
margin: 0 20px 20px;
border-radius: 4px;
.chart {
height: 290px;
}
}
</style>
import { getMetaUserAttrList, getMetaEventList, getTagList, getConnectionList } from '@/api/base'
import { getMetaUserAttrList, getMetaEventList, getTagList, getConnectionList, getUserList } from '@/api/base'
// 用户属性类型
export interface AttrType {
......@@ -93,3 +93,32 @@ export function useConnection() {
})
return { fetchConnectionList, connectionList }
}
// 所有成员
export interface UserType {
sso_id: string
name: string
pinyin: number
}
const userList = ref<UserType[]>([])
export function useUser() {
const [me] = userList.value
const userValue = ref(me?.sso_id)
async function fetchUserList() {
const res = await getUserList()
let { me, students, teachers } = res.data.items
me = { ...me, role: 'me' }
students = students.map((item: any) => {
return { ...item, role: 'student' }
})
teachers = teachers.map((item: any) => {
return { ...item, role: 'teacher' }
})
userValue.value = me.sso_id
userList.value = [me, ...teachers, ...students]
}
onMounted(() => {
if (!userList.value?.length) fetchUserList()
})
return { fetchUserList, userList, userValue }
}
import httpRequest from '@/utils/axios'
// 获取实验详情
export function getExperiment() {
return httpRequest.get('/api/lab/v1/experiment/once/experiment')
}
// 获取实验下的所有事件
export function getEventList() {
return httpRequest.get('/api/lab/v1/experiment/analyse/events')
}
// 事件行为统计
export function getEventActionList(data: { soo_id: string; event_ids: Array<string>; start_date: string; end_date: string }) {
return httpRequest.post('/api/lab/v1/experiment/analyse/event-action-statistics', data)
}
// 事件行为数量走势统计
export function getEventActionTrendList(data: { soo_id: string; event_ids: Array<string>; start_date: string; end_date: string }) {
return httpRequest.post('/api/lab/v1/experiment/analyse/event-action-date-statistics', data)
}
// 事件用户人数统计
export function getEventMemberList(data: { soo_id: string; event_ids: Array<string>; start_date: string; end_date: string }) {
return httpRequest.post('/api/lab/v1/experiment/analyse/event-member-statistics', data)
}
// 事件用户人数统计
export function getEventTimeList(data: { soo_id: string; event_ids: Array<string>; start_date: string; end_date: string }) {
return httpRequest.post('/api/lab/v1/experiment/analyse/event-time-statistics', data)
}
import { getEventList } from '../api'
export interface EventType {
id: string
name: string
english_name: string
}
const eventList = ref<EventType[]>([])
export function useEvent() {
const eventValues = ref([])
async function fetchEventList() {
const res = await getEventList()
eventList.value = res.data.items
}
onMounted(() => {
if (!eventList.value?.length) fetchEventList()
})
return { fetchEventList, eventList, eventValues }
}
import type { RouteRecordRaw } from 'vue-router'
import Layout from '@/components/layout/Index.vue'
const routes: RouteRecordRaw[] = [
{
path: '/analyze/event',
component: Layout,
children: [{ path: '', component: () => import('./views/Index.vue') }]
}
]
export { routes }
<script setup>
import ChartCard from '@/components/ChartCard.vue'
import { DataLine } from '@element-plus/icons-vue'
import { useUser } from '@/composables/useAllData'
import { useEvent } from '../composables/useEvent'
import * as api from '../api'
const { userValue, userList } = useUser()
const { eventValues, eventList } = useEvent()
const date = ref('')
const info = ref()
async function fetchInfo() {
const res = await api.getExperiment()
info.value = res.data.detail
}
onMounted(fetchInfo)
async function handleStart() {
fetchEventAction()
fetchEventActionTrend()
fetchEventMember()
fetchEventTime()
}
const loading = computed(() => {
return loading1.value || loading2.value || loading3.value || loading4.value
})
const params = computed(() => {
const [startDate, endDate] = date.value || []
return { sso_id: userValue.value, event_ids: eventValues.value, start_date: startDate, end_date: endDate }
})
// 事件行为分布
const loading1 = ref(false)
const eventAction = ref([])
async function fetchEventAction() {
if (!userValue.value) return
loading1.value = true
const res = await api.getEventActionList(params.value)
eventAction.value = res.data.items
loading1.value = false
}
const eventActionOption = computed(() => {
if (!eventAction.value.length) return
const value = eventAction.value.map(item => item.total)
const max = Math.max(...value)
return {
grid: { left: '5%', top: '5%', right: '5%', bottom: '5%', containLabel: true },
tooltip: { trigger: 'axis' },
radar: { indicator: eventAction.value.map(item => ({ name: item.group_name, max })) },
series: [
{
name: '事件',
type: 'radar',
tooltip: { trigger: 'item' },
label: { show: true, position: 'top' },
data: [{ value }]
}
]
}
})
// 事件行为数量走势
const loading2 = ref(false)
const eventActionTrend = ref([])
async function fetchEventActionTrend() {
if (!userValue.value) return
loading2.value = true
const res = await api.getEventActionTrendList(params.value)
eventActionTrend.value = res.data.items
loading2.value = false
}
const eventActionTrendOption = computed(() => {
if (!eventActionTrend.value.length) return
const series = eventActionTrend.value.map(group => {
return {
name: group.event_name,
type: 'line',
smooth: true,
label: { show: true, position: 'top' },
data: group.items.map(item => item.total)
}
})
const [first = {}] = eventActionTrend.value || []
return {
grid: { left: '5%', top: '10%', right: '5%', bottom: '5%', containLabel: true },
tooltip: { trigger: 'axis' },
legend: {
bottom: '10',
data: eventActionTrend.value.map(item => item.event_name)
},
xAxis: {
type: 'category',
boundaryGap: ['20%', '20%'],
data: first.items.map(item => item.group_name)
},
yAxis: { type: 'value' },
series
}
})
// 事件用户分布
const loading3 = ref(false)
const eventMember = ref([])
async function fetchEventMember() {
if (!userValue.value) return
loading3.value = true
const res = await api.getEventMemberList(params.value)
eventMember.value = res.data.items
loading3.value = false
}
const eventMemberOption = computed(() => {
if (!eventMember.value.length) return
return {
grid: { left: '5%', top: '10%', right: '5%', bottom: '5%', containLabel: true },
tooltip: { trigger: 'axis' },
xAxis: {
type: 'category',
// axisLabel: { interval: 0, rotate: 30 },
data: eventMember.value.map(item => item.group_name)
},
yAxis: {
type: 'value'
},
series: [
{
name: '用户',
type: 'bar',
label: { show: true, position: 'top' },
itemStyle: { borderRadius: 2 },
data: eventMember.value.map(item => item.total)
}
]
}
})
// 事件发生时间分析
const loading4 = ref(false)
const eventTime = ref([])
async function fetchEventTime() {
if (!userValue.value) return
loading4.value = true
const res = await api.getEventTimeList(params.value)
eventTime.value = res.data.items
loading4.value = false
}
const eventTimeOption = computed(() => {
if (!eventTime.value.length) return
const series = eventTime.value.map(group => {
return {
name: group.event_name,
type: 'line',
// label: { show: true, position: 'top' },
data: group.items.map(item => item.total)
}
})
const [first = {}] = eventTime.value || []
return {
grid: { left: '5%', top: '10%', right: '5%', bottom: '5%', containLabel: true },
tooltip: { trigger: 'axis' },
legend: {
bottom: '10',
data: eventTime.value.map(item => item.event_name)
},
xAxis: {
type: 'category',
boundaryGap: ['20%', '20%'],
data: first.items.map(item => item.group_name)
},
yAxis: { type: 'value' },
series
}
})
</script>
<template>
<AppCard title="事件分析">
<el-form inline label-suffix=":" v-if="info">
<el-form-item label="实验名称">{{ info.name }}</el-form-item>
<el-form-item label="请选择学生/老师">
<el-select v-model="userValue" filterable>
<el-option v-for="item in userList" :label="item.name" :value="item.sso_id" :key="item.sso_id"></el-option>
</el-select>
</el-form-item>
<el-form-item label="时间区间">
<el-date-picker type="monthrange" v-model="date" value-format="YYYY-MM"></el-date-picker>
</el-form-item>
<el-form-item>
<el-button type="primary" :icon="DataLine" :loading="loading" @click="handleStart">分析</el-button>
</el-form-item>
<el-form-item>
<el-checkbox-group v-model="eventValues">
<el-checkbox v-for="item in eventList" :key="item.id" :label="item.id">{{ item.name }}</el-checkbox>
</el-checkbox-group>
</el-form-item>
</el-form>
<el-divider />
<div class="row">
<ChartCard title="事件行为分布" :options="eventActionOption" :loading="loading1"></ChartCard>
<ChartCard title="事件行为数量走势" :options="eventActionTrendOption" :loading="loading2" style="flex: 2.5"></ChartCard>
</div>
<div class="row">
<ChartCard title="事件用户分布" :options="eventMemberOption" :loading="loading3"></ChartCard>
<ChartCard title="事件发生时间分析" :options="eventTimeOption" :loading="loading4" style="flex: 2.5"></ChartCard>
</div>
</AppCard>
</template>
<style lang="scss" scoped>
.total {
font-size: 16px;
font-weight: bold;
color: var(--main-color);
}
.row {
display: flex;
gap: 20px;
& + .row {
margin-top: 20px;
}
}
</style>
import httpRequest from '@/utils/axios'
// 获取实验详情
export function getExperiment() {
return httpRequest.get('/api/lab/v1/experiment/once/experiment')
}
// 获取热门标签
export function getHotTags(params: { sso_id: string; number?: number }) {
return httpRequest.get('/api/lab/v1/experiment/analyse/hot-tags', { params })
}
// 标签人数分析(TOP5)
export function getTagTop(params: { sso_id: string; number?: number }) {
return httpRequest.get('/api/lab/v1/experiment/analyse/tag-top', { params })
}
// 用户标签数分析(TOP10)
export function getMemberTagTop(params: { sso_id: string; number?: number }) {
return httpRequest.get('/api/lab/v1/experiment/analyse/member-tag-top', { params })
}
// 热门群组
export function getHotGroups(params: { sso_id: string; number?: number }) {
return httpRequest.get('/api/lab/v1/experiment/analyse/hot-groups', { params })
}
// 群组人数分析(TOP5)
export function getGroupTop(params: { sso_id: string; number?: number }) {
return httpRequest.get('/api/lab/v1/experiment/analyse/group-top', { params })
}
// 用户群组数分析(TOP10)
export function getMemberGroupTop(params: { sso_id: string; number?: number }) {
return httpRequest.get('/api/lab/v1/experiment/analyse/member-group-top', { params })
}
import type { RouteRecordRaw } from 'vue-router'
import Layout from '@/components/layout/Index.vue'
const routes: RouteRecordRaw[] = [
{
path: '/analyze/label',
component: Layout,
children: [{ path: '', component: () => import('./views/Index.vue') }]
}
]
export { routes }
<script setup>
import ChartCard from '@/components/ChartCard.vue'
import { DataLine } from '@element-plus/icons-vue'
import { useUser } from '@/composables/useAllData'
import * as api from '../api'
const { userValue, userList } = useUser()
const info = ref()
async function fetchInfo() {
const res = await api.getExperiment()
info.value = res.data.detail
}
onMounted(fetchInfo)
watch(userValue, () => {
handleStart()
})
async function handleStart() {
fetchLabelHot()
fetchLabelTop()
fetchLabelMemberTop()
fetchGroupHot()
fetchGroupTop()
fetchGroupMemberTop()
}
const loading = computed(() => {
return loading1.value || loading2.value || loading3.value || loading4.value || loading5.value || loading6.value
})
// 热门标签
const loading1 = ref(false)
const labelHot = ref([])
async function fetchLabelHot() {
if (!userValue.value) return
loading1.value = true
const res = await api.getHotTags({ sso_id: userValue.value })
labelHot.value = res.data.items
loading1.value = false
}
const labelHotOption = computed(() => {
if (!labelHot.value.length) return
return {
grid: { left: '10%', top: '10%', right: '10%', bottom: '10%', containLabel: true },
tooltip: {},
series: [
{
type: 'wordCloud',
gridSize: 15,
sizeRange: [12, 50],
rotationRange: [0, 0],
shape: 'circle',
width: '100%',
height: '100%',
drawOutOfBound: false,
textStyle: {
color: function () {
return 'rgb(' + [Math.round(Math.random() * 160), Math.round(Math.random() * 160), Math.round(Math.random() * 160)].join(',') + ')'
}
},
emphasis: {
focus: 'self',
textStyle: {
textShadowBlur: 10,
textShadowColor: '#333'
}
},
data: labelHot.value.map(item => {
return { name: item.group_name, value: item.total }
})
}
]
}
})
// 标签用户分布Top5
const loading2 = ref(false)
const labelTop = ref([])
async function fetchLabelTop() {
if (!userValue.value) return
loading2.value = true
const res = await api.getTagTop({ sso_id: userValue.value })
labelTop.value = res.data.items
loading2.value = false
}
const labelTopOption = computed(() => {
if (!labelTop.value.length) return
return {
grid: { left: '5%', top: '10%', right: '5%', bottom: '5%', containLabel: true },
tooltip: {
trigger: 'axis'
},
xAxis: {
type: 'category',
axisLabel: { interval: 0 },
data: labelTop.value.map(item => item.group_name)
},
yAxis: {
type: 'value'
},
series: [
{
name: '用户',
type: 'bar',
label: { show: true, position: 'top' },
data: labelTop.value.map(item => item.total)
}
]
}
})
// 用户标签Top10
const loading3 = ref(false)
const labelMemberTop = ref([])
async function fetchLabelMemberTop() {
if (!userValue.value) return
loading3.value = true
const res = await api.getMemberTagTop({ sso_id: userValue.value })
labelMemberTop.value = res.data.items
loading3.value = false
}
const labelMemberTopOption = computed(() => {
if (!labelMemberTop.value.length) return
return {
grid: { left: '5%', top: '10%', right: '5%', bottom: '5%', containLabel: true },
tooltip: {
trigger: 'axis'
},
xAxis: { type: 'value' },
yAxis: {
type: 'category',
inverse: true,
data: labelMemberTop.value.map(item => item.group_name)
},
series: [
{
name: '标签',
type: 'bar',
label: { show: true, position: 'right' },
data: labelMemberTop.value.map(item => item.total)
}
]
}
})
// 热门群组
const loading4 = ref(false)
const groupHot = ref([])
async function fetchGroupHot() {
if (!userValue.value) return
loading4.value = true
const res = await api.getHotGroups({ sso_id: userValue.value })
groupHot.value = res.data.items
loading4.value = false
}
const groupHotOption = computed(() => {
if (!groupHot.value.length) return
return {
grid: { left: '10%', top: '10%', right: '10%', bottom: '10%', containLabel: true },
tooltip: {},
series: [
{
type: 'wordCloud',
gridSize: 15,
sizeRange: [12, 50],
rotationRange: [0, 0],
shape: 'circle',
width: '100%',
height: '100%',
drawOutOfBound: false,
textStyle: {
color: function () {
return 'rgb(' + [Math.round(Math.random() * 160), Math.round(Math.random() * 160), Math.round(Math.random() * 160)].join(',') + ')'
}
},
emphasis: {
focus: 'self',
textStyle: {
textShadowBlur: 10,
textShadowColor: '#333'
}
},
data: groupHot.value.map(item => {
return { name: item.group_name, value: item.total }
})
}
]
}
})
// 群组用户分布Top5
const loading5 = ref(false)
const groupTop = ref([])
async function fetchGroupTop() {
if (!userValue.value) return
loading5.value = true
const res = await api.getGroupTop({ sso_id: userValue.value })
groupTop.value = res.data.items
loading5.value = false
}
const groupTopOption = computed(() => {
if (!groupTop.value.length) return
return {
grid: { left: '5%', top: '10%', right: '5%', bottom: '5%', containLabel: true },
tooltip: {
trigger: 'axis'
},
xAxis: {
type: 'category',
// axisLabel: { interval: 0, rotate: 30 },
data: groupTop.value.map(item => item.group_name)
},
yAxis: {
type: 'value'
},
series: [
{
name: '用户',
type: 'bar',
label: { show: true, position: 'top' },
itemStyle: { borderRadius: 2 },
data: groupTop.value.map(item => item.total)
}
]
}
})
// 用户群组Top10
const loading6 = ref(false)
const groupMemberTop = ref([])
async function fetchGroupMemberTop() {
if (!userValue.value) return
loading6.value = true
const res = await api.getMemberGroupTop({ sso_id: userValue.value })
groupMemberTop.value = res.data.items
loading6.value = false
}
const groupMemberTopOption = computed(() => {
if (!groupMemberTop.value.length) return
return {
grid: { left: '5%', top: '10%', right: '5%', bottom: '5%', containLabel: true },
tooltip: {
trigger: 'axis'
},
xAxis: { type: 'value' },
yAxis: {
type: 'category',
inverse: true,
data: groupMemberTop.value.map(item => item.group_name)
},
series: [
{
name: '群组',
type: 'bar',
label: { show: true, position: 'right' },
data: groupMemberTop.value.map(item => item.total)
}
]
}
})
</script>
<template>
<AppCard title="标签群组分析">
<el-form inline label-suffix=":">
<el-form-item label="实验名称">{{ info?.name }}</el-form-item>
<el-form-item label="请选择学生/老师">
<el-select v-model="userValue" filterable>
<el-option v-for="item in userList" :label="item.name" :value="item.sso_id" :key="item.sso_id"></el-option>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" :icon="DataLine" :loading="loading" @click="handleStart">分析</el-button>
</el-form-item>
</el-form>
<el-divider />
<div class="row">
<ChartCard title="热门标签" :options="labelHotOption" :loading="loading1"></ChartCard>
<ChartCard title="标签用户分布Top5" :options="labelTopOption" :loading="loading2"></ChartCard>
<ChartCard title="用户标签Top10" :options="labelMemberTopOption" :loading="loading3"></ChartCard>
</div>
<div class="row">
<ChartCard title="热门群组" :options="groupHotOption" :loading="loading4"></ChartCard>
<ChartCard title="群组用户分布Top5" :options="groupTopOption" :loading="loading5"></ChartCard>
<ChartCard title="用户群组Top10" :options="groupMemberTopOption" :loading="loading6"></ChartCard>
</div>
</AppCard>
</template>
<style lang="scss" scoped>
.total {
font-size: 16px;
font-weight: bold;
color: var(--main-color);
}
.row {
display: flex;
gap: 20px;
& + .row {
margin-top: 20px;
}
}
</style>
import httpRequest from '@/utils/axios'
// 获取实验详情
export function getExperiment() {
return httpRequest.get('/api/lab/v1/experiment/once/experiment')
}
// 获取实验下的所有事件
export function getEventList() {
return httpRequest.get('/api/lab/v1/experiment/analyse/events')
}
// 事件行为分布
export function getEventAction(params: { start_date: string; end_date: string; sso_id: string; event_id: string }) {
return httpRequest.get('/api/lab/v1/experiment/analyse/event-marketing-statistics', { params })
}
// 实验营销事件漏斗分析
export function getEventMarketing(params: { start_date: string; end_date: string; sso_id: string; event_id: string }) {
return httpRequest.get('/api/lab/v1/experiment/analyse/event-funnel-statistics', { params })
}
// 获取实验营销事件漏斗配置
export function getEventFunnel() {
return httpRequest.get('/api/lab/v1/experiment/analyse/event-funnels')
}
// 保存实验营销事件漏斗配置
export function saveEventFunnel(data: { rules: Array<{ id: string; event_id: string; sort: number; name: string }> }) {
return httpRequest.post('/api/lab/v1/experiment/analyse/save-event-funnels', data)
}
// 事件行为分布
export function getMemberList(params: { start_date: string; end_date: string; sso_id: string; event_id: string }) {
return httpRequest.get('/api/lab/v1/experiment/analyse/member-retention-statistics', { params })
}
<script setup>
import { SemiSelect } from '@element-plus/icons-vue'
import { useEvent } from '../composables/useEvent'
import { getEventFunnel, saveEventFunnel } from '../api'
const { eventList } = useEvent()
const rules = ref([])
async function fetchConfig() {
const res = await getEventFunnel()
rules.value = res.data.items
}
onMounted(() => {
fetchConfig()
})
function handleAdd() {
rules.value.push({ id: '0', event_id: '', sort: 0, name: '' })
}
function handleRemove(index) {
rules.value.splice(index, 1)
}
async function handelSubmit() {
const paramsRules = rules.value.map((item, index) => {
return { ...item, sort: index + 1 }
})
await saveEventFunnel({ rules: paramsRules })
}
</script>
<template>
<el-dialog title="创建营销漏斗分析" width="600">
<el-button type="primary" @click="handleAdd">添加漏斗步数</el-button>
<div class="rule-item" v-for="(item, index) in rules" :key="index">
<el-button type="primary">{{ index + 1 }}</el-button>
<el-input v-model="item.name" :placeholder="'漏斗第' + (index + 1) + '步名字'"></el-input>
<el-select placeholder="请选择事件" v-model="item.event_id">
<el-option v-for="item in eventList" :key="item.id" :label="item.name" :value="item.id"></el-option>
</el-select>
<el-button :icon="SemiSelect" @click="handleRemove(index)"></el-button>
</div>
<template #footer>
<el-button type="primary" @click="handelSubmit">保存</el-button>
</template>
</el-dialog>
</template>
<style lang="scss" scoped>
.rule-item {
margin: 10px 0;
gap: 10px;
display: flex;
}
</style>
import { getEventList } from '../api'
export interface EventType {
id: string
name: string
english_name: string
}
const eventList = ref<EventType[]>([])
export function useEvent() {
const eventValue = ref('')
async function fetchEventList() {
const res = await getEventList()
eventList.value = res.data.items
}
onMounted(() => {
if (!eventList.value?.length) fetchEventList()
})
return { fetchEventList, eventList, eventValue }
}
import type { RouteRecordRaw } from 'vue-router'
import Layout from '@/components/layout/Index.vue'
const routes: RouteRecordRaw[] = [
{
path: '/analyze/marketing',
component: Layout,
children: [{ path: '', component: () => import('./views/Index.vue') }]
}
]
export { routes }
差异被折叠。
......@@ -5,51 +5,27 @@ export function getExperiment() {
return httpRequest.get('/api/lab/v1/experiment/once/experiment')
}
// 获取成员性别
export function getMemberGender() {
return httpRequest.get('/api/lab/v1/experiment/once/member-gender-statistics')
// 获取用户性别
export function getMemberGender(params: { sso_id: string }) {
return httpRequest.get('/api/lab/v1/experiment/analyse/gender', { params })
}
// 获取成员年龄
export function getMemberAge() {
return httpRequest.get('/api/lab/v1/experiment/once/member-age-statistics')
// 获取用户来源
export function getMemberConnections(params: { sso_id: string }) {
return httpRequest.get('/api/lab/v1/experiment/analyse/connections', { params })
}
// 获取成员信用卡等级
export function getCardLevel() {
return httpRequest.get('/api/lab/v1/experiment/once/member-credit-card-level-statistics')
// 获取用户状态
export function getMemberStatus(params: { sso_id: string }) {
return httpRequest.get('/api/lab/v1/experiment/analyse/member-status', { params })
}
// 获取成员月收入
export function getMemberMonthly() {
return httpRequest.get('/api/lab/v1/experiment/once/member-monthly-income-statistics')
// 获取用户元数据属性
export function getMemberAttrs(params: { sso_id: string; member_meta_id: string }) {
return httpRequest.get('/api/lab/v1/experiment/analyse/member-attrs', { params })
}
// 获取标签
export function getLabel() {
return httpRequest.get('/api/lab/v1/experiment/once/tag-member-statistics')
}
// 获取群组
export function getGroup() {
return httpRequest.get('/api/lab/v1/experiment/once/group-member-statistics')
}
// 获取事件
export function getEvent() {
return httpRequest.get('/api/lab/v1/experiment/once/event-member-statistics')
}
// 获取信用卡开卡
export function getCardOpen() {
return httpRequest.get('/api/lab/v1/experiment/once/credit-card-open-statistics')
}
// 获取信用卡积分
export function getCardIntegral() {
return httpRequest.get('/api/lab/v1/experiment/once/credit-card-integral-statistics')
}
// 获取信用卡分期还款
export function getCardInstallment() {
return httpRequest.get('/api/lab/v1/experiment/once/credit-card-installment-statistics')
// 获取用户元数据属性
export function getMemberMetaAttrs() {
return httpRequest.get('/api/lab/v1/experiment/analyse/member-meta-attrs')
}
<script setup>
import ChartCard from '@/components/ChartCard.vue'
import { getMemberAttrs } from '../api'
import { useMetaAttr } from '../composables/useMetaAttr'
const props = defineProps({ ssoId: String })
const { metaAttrList, metaAttrValue } = useMetaAttr()
const loading = ref(false)
const attrs = ref([])
async function fetchAttrs() {
if (!props.ssoId || !metaAttrValue.value) return
loading.value = true
const res = await getMemberAttrs({ sso_id: props.ssoId, member_meta_id: metaAttrValue.value })
attrs.value = res.data.items.map(item => {
return { name: item.group_name, value: item.total }
})
loading.value = false
}
watchEffect(() => {
fetchAttrs()
})
const chartTypeList = [
{ value: 1, label: '柱状图' },
{ value: 2, label: '线状图' },
{ value: 3, label: '面积图' },
{ value: 4, label: '饼状图' },
{ value: 5, label: '环形图' }
]
const chartType = ref(1)
const options = computed(() => {
if (!attrs.value.length) return
if (chartType.value === 1) {
// 柱状图
return {
grid: { left: '5%', top: '10%', right: '5%', bottom: '5%', containLabel: true },
tooltip: { trigger: 'axis' },
xAxis: {
type: 'category',
data: attrs.value.map(item => item.name)
},
yAxis: { type: 'value' },
series: [
{
name: '用户',
type: 'bar',
label: { show: true, position: 'top' },
data: attrs.value.map(item => item.value)
}
]
}
} else if (chartType.value === 2) {
// 线状图
return {
grid: { left: '5%', top: '10%', right: '5%', bottom: '5%', containLabel: true },
tooltip: { trigger: 'axis' },
xAxis: {
type: 'category',
data: attrs.value.map(item => item.name)
},
yAxis: { type: 'value' },
series: [
{
name: '用户',
type: 'line',
label: { show: true, position: 'top' },
data: attrs.value.map(item => item.value)
}
]
}
} else if (chartType.value === 3) {
// 面积图
return {
grid: { left: '5%', top: '10%', right: '5%', bottom: '5%', containLabel: true },
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross',
label: {
backgroundColor: '#6a7985'
}
}
},
xAxis: {
type: 'category',
boundaryGap: false,
data: attrs.value.map(item => item.name)
},
yAxis: { type: 'value' },
series: [
{
name: '用户',
type: 'line',
areaStyle: {},
label: { show: true, position: 'top' },
data: attrs.value.map(item => item.value)
}
]
}
} else if (chartType.value === 4) {
return {
tooltip: { trigger: 'item', formatter: '{b}: {c}<br />{d}%' },
series: [
{
type: 'pie',
label: { formatter: '{b}\n{d}%' },
itemStyle: { borderRadius: 6 },
radius: [0, '70%'],
data: attrs.value
}
]
}
} else {
return {
tooltip: { trigger: 'item', formatter: '{b}: {c}<br />{d}%' },
series: [
{
type: 'pie',
label: { formatter: '{b}\n{d}%' },
itemStyle: { borderRadius: 6 },
radius: ['40%', '70%'],
data: attrs.value
}
]
}
}
})
</script>
<template>
<ChartCard :options="options" :loading="loading">
<template #title>
<el-row justify="space-between" style="flex: 1">
<el-select v-model="metaAttrValue" filterable>
<el-option v-for="item in metaAttrList" :label="item.name" :value="item.id" :key="item.id"></el-option>
</el-select>
<el-select v-model="chartType" style="width: 86px">
<el-option v-for="item in chartTypeList" :key="item.value" :label="item.label" :value="item.value"></el-option>
</el-select>
</el-row>
</template>
</ChartCard>
</template>
import { getMemberMetaAttrs } from '../api'
// 所有成员
export interface UserType {
id: string
name: string
english_name: string
type: string
format: string
}
const metaAttrList = ref<UserType[]>([])
export function useMetaAttr() {
const metaAttrValue = ref('')
async function fetchMetaAttrList() {
const res = await getMemberMetaAttrs()
metaAttrList.value = res.data.items
}
onMounted(() => {
if (!metaAttrList.value?.length) fetchMetaAttrList()
})
return { fetchMetaAttrList, metaAttrList, metaAttrValue }
}
......@@ -65,7 +65,9 @@ const listOptions = computed(() => {
label: '状态',
prop: 'status_name',
computed: (row: any) => {
return row.row.status === '0' ? `<span style="color: rgb(170, 2, 49)">${row.row.status_name}</span>` : `<span style="color: #00ac27">${row.row.status_name}</span>`
return row.row.status === '0'
? `<span style="color: rgb(170, 2, 49)">${row.row.status_name}</span>`
: `<span style="color: #00ac27">${row.row.status_name}</span>`
}
},
{ label: '更新人', prop: 'updated_operator_name' },
......@@ -203,38 +205,41 @@ const downloadMember = function (isAll?: boolean) {
<AppCard>
<AppList v-bind="listOptions" ref="appList" @selection-change="handleSelectionChange">
<template #header-buttons>
<el-space>
<el-button type="primary" :icon="Plus" @click="handleAdd" v-permission="'v1-experiment-member-create'">新建</el-button>
<el-dropdown v-permission="'v1-experiment-member-download'">
<el-button type="primary" :icon="Download">导出</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="downloadMember(true)">全部用户数据</el-dropdown-item>
<el-dropdown-item :disabled="!multipleSelection.length" @click="downloadMember(false)">勾选用户数据</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<el-dropdown v-permission="['v1-experiment-member-member-upload', 'v1-experiment-member-event-upload']">
<el-button type="primary" :icon="Upload">导入</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="userVisible = true">用户数据</el-dropdown-item>
<el-dropdown-item @click="eventsVisible = true">用户事件数据</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<el-button type="primary" @click="progressVisible = true" v-permission="'v1-experiment-member-tasks'">数据导入进度</el-button>
<!-- <el-button type="danger" plain :icon="Delete" :disabled="!multipleSelection.length" @click="handleRemoves()" v-permission="'v1-experiment-member-delete'">删除</el-button> -->
<el-dropdown v-permission="'v1-experiment-member-delete'">
<el-button type="danger" :icon="Delete">删除</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="handleRemoves(true)">删除全部用户</el-dropdown-item>
<el-dropdown-item :disabled="!multipleSelection.length" @click="handleRemoves(false)">删除勾选用户</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</el-space>
<el-row justify="space-between">
<el-space>
<el-button type="primary" :icon="Plus" @click="handleAdd" v-permission="'v1-experiment-member-create'">新建</el-button>
<el-dropdown v-permission="'v1-experiment-member-download'">
<el-button type="primary" :icon="Download">导出</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="downloadMember(true)">全部用户数据</el-dropdown-item>
<el-dropdown-item :disabled="!multipleSelection.length" @click="downloadMember(false)">勾选用户数据</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<el-dropdown v-permission="['v1-experiment-member-member-upload', 'v1-experiment-member-event-upload']">
<el-button type="primary" :icon="Upload">导入</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="userVisible = true">用户数据</el-dropdown-item>
<el-dropdown-item @click="eventsVisible = true">用户事件数据</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<el-button type="primary" @click="progressVisible = true" v-permission="'v1-experiment-member-tasks'">数据导入进度</el-button>
<!-- <el-button type="danger" plain :icon="Delete" :disabled="!multipleSelection.length" @click="handleRemoves()" v-permission="'v1-experiment-member-delete'">删除</el-button> -->
<el-dropdown v-permission="'v1-experiment-member-delete'">
<el-button type="danger" :icon="Delete">删除</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="handleRemoves(true)">删除全部用户</el-dropdown-item>
<el-dropdown-item :disabled="!multipleSelection.length" @click="handleRemoves(false)">删除勾选用户</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</el-space>
<router-link to="/analyze/user"><el-button type="primary">用户分析</el-button></router-link>
</el-row>
</template>
<template #table-x="{ row }">
<el-button type="primary" plain @click="handleImage(row)">画像</el-button>
......
......@@ -25,7 +25,7 @@ router.beforeEach(async (to, from, next) => {
path: to.path,
query: {
...to.query,
experiment_id: from.query.experiment_id || '7025368348925886464',
experiment_id: from.query.experiment_id || '7165149417073278976',
student_id: from.query.student_id,
force_tgc: from.query.force_tgc
}
......
......@@ -3,7 +3,7 @@ import { ElMessage } from 'element-plus'
// import router from '@/router'
const httpRequest = axios.create({
timeout: 60000,
// timeout: 60000,
withCredentials: true,
headers: {
// 'Content-Type': 'application/x-www-form-urlencoded'
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论