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

chore: 更新首页显示内容

上级 2d05d23e
...@@ -20,10 +20,15 @@ use([ ...@@ -20,10 +20,15 @@ use([
TitleComponent, TitleComponent,
TooltipComponent, TooltipComponent,
LegendComponent, LegendComponent,
GridComponent GridComponent,
]) ])
const props = defineProps<{ title?: string; options?: any; loading?: boolean }>() const props = withDefaults(
defineProps<{ title?: string; options?: any; loading?: boolean; hasFullscreen?: boolean }>(),
{
hasFullscreen: true,
}
)
const el = ref<HTMLElement | null>(null) const el = ref<HTMLElement | null>(null)
const { isFullscreen, toggle } = useFullscreen(el) const { isFullscreen, toggle } = useFullscreen(el)
...@@ -42,7 +47,7 @@ const color = ['#af1c40', '#c17933', '#8f0034', '#d45548', '#ab3259', '#dec34c', ...@@ -42,7 +47,7 @@ const color = ['#af1c40', '#c17933', '#8f0034', '#d45548', '#ab3259', '#dec34c',
</slot> </slot>
<div class="tools"> <div class="tools">
<slot name="tools"></slot> <slot name="tools"></slot>
<el-tooltip effect="dark" :content="isFullscreen ? '退出全屏' : '全屏'"> <el-tooltip effect="dark" :content="isFullscreen ? '退出全屏' : '全屏'" v-if="hasFullscreen">
<el-icon class="icon-fullscreen" @click="toggle"><FullScreen /></el-icon> <el-icon class="icon-fullscreen" @click="toggle"><FullScreen /></el-icon>
</el-tooltip> </el-tooltip>
</div> </div>
......
...@@ -14,3 +14,13 @@ export function getMembersList() { ...@@ -14,3 +14,13 @@ export function getMembersList() {
export function getEventList(params: { member_id: string; page?: number; 'per-page'?: number }) { 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 })
} }
// 获取用户来源
export function getMemberConnections(params: { sso_id: string }) {
return httpRequest.get('/api/lab/v1/experiment/analyse/connections', { params })
}
// 获取热门标签
export function getHotTags(params: { sso_id: string; number?: number }) {
return httpRequest.get('/api/lab/v1/experiment/analyse/hot-tags', { params })
}
\ No newline at end of file
<script setup lang="ts"> <script setup>
import { getExperimentData, getMembersList, getEventList } from '../api' import { getExperimentData, getMembersList, getEventList, getMemberConnections, getHotTags } from '../api'
import Icon from '@/components/ConnectionIcon.vue' import Icon from '@/components/ConnectionIcon.vue'
// import { useMapStore } from '@/stores/map' import ChartCard from '@/components/ChartCard.vue'
import { useMapStore } from '@/stores/map'
import { useUserStore } from '@/stores/user'
import { getNameByValue } from '@/utils/dictionary'
const ViewEvent = defineAsyncComponent(() => import('@/components/ViewEvent.vue')) const ViewEvent = defineAsyncComponent(() => import('@/components/ViewEvent.vue'))
const connectionTypeList = useMapStore().getMapValuesByKey('experiment_connection_type')
const materialTypeList = useMapStore().getMapValuesByKey('experiment_marketing_material_type')
const userStore = useUserStore()
// 左边展示数据 // 左边展示数据
let leftData = $ref<{ let leftData = $ref()
members: string getExperimentData().then((res) => {
tags: string
groups: string
files: string
itineraries: string
connections: string
}>()
getExperimentData().then(res => {
leftData = res.data leftData = res.data
}) })
// 最近活跃客户 // 最近活跃客户
let userList = $ref<{ name: string; id: string; isActive: boolean; gender: string }[]>([]) let userList = $ref([])
getMembersList().then(res => { getMembersList().then((res) => {
userList = res.data.map((element: any, index: number) => { userList = res.data.map((element, index) => {
element.isActive = index === 0 element.isActive = index === 0
return element return element
}) })
}) })
const activeUser = computed(() => { const activeUser = computed(() => {
return userList.find(item => item.isActive) return userList.find((item) => item.isActive)
}) })
// 最近活跃客户的事件 // 最近活跃客户的事件
let eventData = $ref<{ list: Array<any>; total: number }>({ list: [], total: 0 }) let eventData = $ref({ list: [], total: 0 })
const eventCurrentPage = ref(1) const eventCurrentPage = ref(1)
const getEvent = function (id?: string) { const getEvent = function (id) {
id = id || activeUser.value?.id id = id || activeUser.value?.id
if (id) { if (id) {
getEventList({ member_id: id, page: eventCurrentPage.value, 'per-page': 10 }).then(res => { getEventList({ member_id: id, page: eventCurrentPage.value, 'per-page': 4 }).then((res) => {
eventData = res.data eventData = res.data
}) })
} }
...@@ -45,14 +45,14 @@ watchEffect(() => { ...@@ -45,14 +45,14 @@ watchEffect(() => {
}) })
// 切换客户事件 // 切换客户事件
const handleUser = (item: any) => { const handleUser = (item) => {
userList?.map(item => (item.isActive = false && item)) userList?.map((item) => (item.isActive = false && item))
item.isActive = true item.isActive = true
eventCurrentPage.value = 1 eventCurrentPage.value = 1
} }
// 获取上下午 // 获取上下午
const getDate = function (date: string) { const getDate = function (date) {
return parseInt(date.slice(date.indexOf(' '), date.indexOf(' ') + 3)) > 12 ? '下午' : '上午' return parseInt(date.slice(date.indexOf(' '), date.indexOf(' ') + 3)) > 12 ? '下午' : '上午'
} }
...@@ -61,53 +61,250 @@ const getDate = function (date: string) { ...@@ -61,53 +61,250 @@ const getDate = function (date: string) {
// return experimentTypeList.find((item: any) => item.id === id)?.value as string // return experimentTypeList.find((item: any) => item.id === id)?.value as string
// } // }
const currentUser = computed(() => { const currentUser = computed(() => {
return userList?.find(item => item.isActive) return userList?.find((item) => item.isActive)
}) })
const viewEventVisible = ref(false) const viewEventVisible = ref(false)
const currentViewEvent = ref() const currentViewEvent = ref()
function handleViewEvent(item: any) { function handleViewEvent(item) {
viewEventVisible.value = true viewEventVisible.value = true
currentViewEvent.value = item currentViewEvent.value = item
} }
// 用户数据来源分布
const connection = ref([])
async function fetchConnections() {
const res = await getMemberConnections({ sso_id: userStore.user?.id })
connection.value = res.data.items.map((item) => {
return { ...item, group_name: getNameByValue(item.group_name, connectionTypeList) }
})
}
const connectionOption = computed(() => {
if (!connection.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: connection.value.map((item) => item.group_name),
},
yAxis: {
type: 'value',
},
series: [
{
name: '数据',
type: 'bar',
label: { show: true, position: 'top' },
data: connection.value.map((item) => item.total),
},
],
}
})
// 热门标签
const labelHot = ref([])
async function fetchLabelHot() {
const res = await getHotTags({ sso_id: userStore.user?.id })
labelHot.value = res.data.items
}
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 }
}),
},
],
}
})
// 营销物料分布
const materialOptions = computed(() => {
if (!leftData?.materials.length) return
return {
grid: { left: '5%', top: '10%', right: '5%', bottom: '5%', containLabel: true },
tooltip: {
trigger: 'item',
formatter: '{b}: {c}<br />{d}%',
},
series: [
{
type: 'pie',
label: { formatter: '{b}\n{d}%' },
itemStyle: { borderRadius: 6 },
radius: [0, '70%'],
data: leftData.materials.map((item) => {
return { name: getNameByValue(item.type, materialTypeList), value: item.num }
}),
},
],
}
})
// 直播场次分布
const liveNumberOptions = computed(() => {
if (!leftData?.live_practice_records.length) return
return {
grid: { left: '5%', top: '10%', right: '5%', bottom: '5%', containLabel: true },
tooltip: {
trigger: 'item',
formatter: '{b}: {c}<br />{d}%',
},
series: [
{
type: 'pie',
label: { formatter: '{b}\n{d}%' },
itemStyle: { borderRadius: 6 },
radius: [0, '70%'],
data: leftData.live_practice_records.map((item) => {
return { name: item.live_commodity, value: item.num }
}),
},
],
}
})
onMounted(() => {
fetchConnections()
fetchLabelHot()
})
</script> </script>
<template> <template>
<div class="home"> <div class="home">
<div class="home-left_content"> <div class="row">
<div class="content-card"> <div class="col">
<AppCard class="card" title="我的客户:"> <div class="row">
<p>{{ leftData?.members }}</p> <AppCard class="card" title="链接渠道">
<p>
<router-link to="/connect">{{ leftData?.connections }}</router-link>
</p>
</AppCard> </AppCard>
<AppCard class="card" title="我的标签:"> <AppCard class="card" title="用户属性">
<p>{{ leftData?.tags }}</p> <p>
<router-link to="/metadata/user">{{ leftData?.user_attributes }}</router-link>
</p>
</AppCard> </AppCard>
<AppCard class="card" title="我的群组:"> <AppCard class="card" title="用户行为事件">
<p>{{ leftData?.groups }}</p> <p>
<router-link to="/metadata/event">{{ leftData?.events }}</router-link>
</p>
</AppCard>
<AppCard class="card" title="标签">
<p>
<router-link to="/label">{{ leftData?.tags }}</router-link>
</p>
</AppCard>
<AppCard class="card" title="营销策划报告">
<p>
<router-link to="/market/my">{{ leftData?.marketing_planning_records }}</router-link>
</p>
</AppCard> </AppCard>
</div> </div>
<div class="content-card"> <div class="row">
<AppCard class="card" title="我的营销资料:"> <AppCard class="card" title="用户旅程">
<p>{{ leftData?.files }}</p> <p>
<router-link to="/trip">{{ leftData?.itineraries }}</router-link>
</p>
</AppCard>
<AppCard class="card" title="用户数">
<p>
<router-link to="/user">{{ leftData?.members }}</router-link>
</p>
</AppCard> </AppCard>
<AppCard class="card" title="我的旅程:"> <AppCard class="card" title="用户行为事件数">
<p>{{ leftData?.itineraries }}</p> <p>{{ leftData?.user_events }}</p>
</AppCard> </AppCard>
<AppCard class="card" title="我的链接:"> <AppCard class="card" title="用户群组">
<p>{{ leftData?.connections }}</p> <p>
<router-link to="/group">{{ leftData?.groups }}</router-link>
</p>
</AppCard>
<AppCard class="card" title="直播商品">
<p>
<router-link to="/live/product/management">{{ leftData?.live_commodities }}</router-link>
</p>
</AppCard> </AppCard>
</div> </div>
<AppCard title="当前用户旅程模板数量:"><el-empty :image-size="120" /></AppCard> <div class="row">
<AppCard title="旅程转化目标分析:"><el-empty :image-size="120" /></AppCard> <AppCard class="card" title="文本营销物料">
<p>
<router-link to="/material?type=1">{{ leftData?.text_materials }}</router-link>
</p>
</AppCard>
<AppCard class="card" title="图片营销物料">
<p>
<router-link to="/material?type=2">{{ leftData?.pic_materials }}</router-link>
</p>
</AppCard>
<AppCard class="card" title="视频营销物料">
<p>
<router-link to="/material?type=4">{{ leftData?.video_materials }}</router-link>
</p>
</AppCard>
<AppCard class="card" title="直播话术">
<p>
<router-link to="/live/talk">{{ leftData?.live_speeches }}</router-link>
</p>
</AppCard>
<AppCard class="card" title="直播">
<p>
<router-link to="/live">{{ leftData?.live_practice }}</router-link>
</p>
</AppCard>
</div> </div>
<div class="home-right_content"> </div>
<div style="width: 340px">
<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)"> <template v-for="(item, index) in userList" :key="item.id">
<div
:class="{ 'content-user_item': true, active: item.isActive }"
@click="handleUser(item)"
v-if="index < 8">
<img <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'" /> :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>
</template>
</div> </div>
<template v-if="eventData.total"> <template v-if="eventData.total">
<div class="event-box" v-for="item in eventData.list" :key="item.id"> <div class="event-box" v-for="item in eventData.list" :key="item.id">
...@@ -127,29 +324,52 @@ function handleViewEvent(item: any) { ...@@ -127,29 +324,52 @@ function handleViewEvent(item: any) {
</div> </div>
</div> </div>
<div style="display: flex; align-items: center; justify-content: center; margin-top: 20px"> <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 /> <el-pagination
layout="prev, pager, next"
v-model:current-page="eventCurrentPage"
:total="eventData.total"
hide-on-single-page />
</div> </div>
</template> </template>
<el-empty description="暂无数据" :image-size="80" v-else /> <el-empty description="暂无数据" :image-size="80" v-else />
</AppCard> </AppCard>
</div> </div>
</div> </div>
<div class="row">
<ChartCard title="用户来源渠道分布" :options="connectionOption" :hasFullscreen="false"></ChartCard>
<ChartCard title="用户标签分布" :options="labelHotOption" :hasFullscreen="false"></ChartCard>
<ChartCard title="营销物料分布" :options="materialOptions" :hasFullscreen="false"></ChartCard>
<ChartCard title="直播场次分布" :options="liveNumberOptions" :hasFullscreen="false"></ChartCard>
</div>
</div>
<!-- 事件详情 --> <!-- 事件详情 -->
<ViewEvent v-model="viewEventVisible" :event="currentViewEvent" :user="currentUser" v-if="viewEventVisible && currentViewEvent"></ViewEvent> <ViewEvent
v-model="viewEventVisible"
:event="currentViewEvent"
:user="currentUser"
v-if="viewEventVisible && currentViewEvent"></ViewEvent>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
.home { .home {
.row {
display: flex; display: flex;
.home-left_content { gap: 10px;
+ .row {
margin-top: 10px;
}
.col {
flex: 1; flex: 1;
.content-card {
display: flex; display: flex;
margin-bottom: 10px; flex-direction: column;
gap: 10px; .row {
flex: 1;
}
}
.card { .card {
flex: 1; flex: 1;
margin: 0; margin: 0;
min-height: 100%;
p { p {
line-height: 100px; line-height: 100px;
text-align: center; text-align: center;
...@@ -158,32 +378,28 @@ function handleViewEvent(item: any) { ...@@ -158,32 +378,28 @@ function handleViewEvent(item: any) {
} }
} }
} }
::v-deep(.chart-card) {
background-color: #fff;
} }
.home-right_content { }
width: 40%; .content-user {
margin-left: 10px;
.card {
height: 100%;
.content-user {
background: #efefef; background: #efefef;
padding: 20px; padding: 10px;
border-radius: 5px; border-radius: 5px;
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
.content-user_item { .content-user_item {
width: 60px; width: 50px;
// background-color: #fff;
padding: 10px; padding: 10px;
border-radius: 10px; border-radius: 10px;
margin-right: 12px;
cursor: pointer; cursor: pointer;
&.active { &.active {
background-color: #fff; background-color: #fff;
} }
} }
img { img {
width: 50px; width: 40px;
height: 50px; height: 40px;
margin: 0 auto; margin: 0 auto;
display: block; display: block;
} }
...@@ -192,9 +408,6 @@ function handleViewEvent(item: any) { ...@@ -192,9 +408,6 @@ function handleViewEvent(item: any) {
font-size: 14px; font-size: 14px;
margin-top: 10px; margin-top: 10px;
} }
}
}
}
} }
.event-box { .event-box {
border-bottom: 1px solid #ccc; border-bottom: 1px solid #ccc;
...@@ -207,7 +420,6 @@ function handleViewEvent(item: any) { ...@@ -207,7 +420,6 @@ function handleViewEvent(item: any) {
display: flex; display: flex;
font-size: 12px; font-size: 12px;
align-items: center; align-items: center;
// margin-bottom: 20px;
flex-wrap: wrap; flex-wrap: wrap;
.event { .event {
display: flex; display: flex;
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论