Files
lingji-work-fe/src/views/components/dataEngine/userPersona.vue
2025-07-14 09:46:16 +08:00

636 lines
19 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view>
<topHeader ref="topHeaderRef" @search="search"></topHeader>
<div class="h-360px w-100% flex mb-24px">
<!-- 1. 性别分布 -->
<div class="bg-#fff rounded-8px border-1px border-#D7D7D9 border-solid w-100% py-0 px-20px w-600px mr-24px">
<div class="title-row">
<span class="title mr-4px">性别分布</span>
<a-tooltip>
<template #content>基于社交内容平台中用户资料互动行为及语义特征进行智能识别与估算</template>
<icon-question-circle size="16" class="!color-#737478" />
</a-tooltip>
</div>
<a-space v-if="genderData.length > 0">
<div id="container" class="w-300px h-300px"></div>
<a-space direction="vertical" style="font-size: 14px">
<a-space>
<span style="width: 8px; height: 8px; background-color: #f64b31; border-radius: 50%"></span>
<span>女性</span>
<span v-if="genderData.length > 0" style="width: 40px">{{ genderData[0].rate * 100 }}%</span>
<span>TGI</span>
<span v-if="genderData.length > 0">{{ genderData[0].tgi }}</span>
</a-space>
<a-space>
<span style="width: 8px; height: 8px; background-color: #2a59f3; border-radius: 50%"></span>
<span>男性</span>
<span v-if="genderData.length > 1" style="width: 40px">{{ genderData[1].rate * 100 }}%</span>
<span>TGI</span>
<span v-if="genderData.length > 1">{{ genderData[1].tgi }}</span>
</a-space>
</a-space>
</a-space>
<div v-else>
<NoData class="w-100% h-100%" />
</div>
</div>
<!-- 2. 年龄分布 -->
<div class="bg-#fff rounded-8px border-1px border-#D7D7D9 border-solid w-100% py-0 px-20px flex-1 flex flex-col">
<a-space style="display: flex; justify-content: space-between; width: 100%; font-size: 12px">
<div class="title-row">
<span class="title mr-4px">年龄分布</span>
<a-tooltip>
<template #content>基于社交平台的公开信息内容偏好与行为模式通过算法进行年龄段归类和统计</template>
<icon-question-circle size="16" class="!color-#737478" />
</a-tooltip>
</div>
<a-space v-if="ageValueData.length > 0" align="center">
<span style="width: 16px; height: 8px; background-color: #6d4cfe; border-radius: 2px"></span>
<span style="color: #6d4cfe">占比</span>
<span style="width: 16px; height: 8px; background-color: #f64b31; border-radius: 2px"></span>
<span style="color: #f64b31">TGI比</span>
</a-space>
</a-space>
<div v-if="ageValueData.length === 0" class="w-100% flex-1">
<NoData />
</div>
<div v-else id="age-container" class="w-100% flex-1"></div>
</div>
</div>
<div class="bg-#fff rounded-8px border-1px border-#D7D7D9 border-solid w-100% py-0 px-20px flex-1 pb-20px">
<div class="title-row">
<span class="title mr-4px">地域分布</span>
<a-tooltip>
<template #content>基于社交平台的IP归属地位置标签内容发布地等数据推测用户活跃区域</template>
<icon-question-circle size="16" class="!color-#737478" />
</a-tooltip>
</div>
<div class="flex">
<a-space direction="vertical">
<div id="chinaMap" style="height: 416px; width: 640px"></div>
<a-space direction="vertical" style="font-size: 14px">
<span class="cts">搜索指数</span>
<a-space>
<span class="cts"></span>
<span
v-for="item in 5"
:key="item"
:style="{
width: `${18 - item * 2}px`,
height: `${18 - item * 2}px`,
backgroundColor: '#6D4CFE',
borderRadius: '50%',
display: 'inline-block',
margin: '0 2px',
}"
></span>
<span class="cts"></span>
</a-space>
</a-space>
</a-space>
<div class="flex flex-col h-486px">
<a-tabs default-active-key="1" class="h-100%" @change="tabChange">
<a-tab-pane key="1" title="省份">
<a-table :data="geoList" :pagination="false" class="h-100%" :scroll="{ y: '100%' }">
<template #empty>
<NoData />
</template>
<template #columns>
<a-table-column title="排名" data-index="rank" />
<a-table-column title="省份" data-index="geo" />
<a-table-column title="分布占比" data-index="rate" />
<a-table-column title="TGI指数" data-index="tgi" />
</template>
</a-table>
</a-tab-pane>
<a-tab-pane key="2" title="城市">
<a-table :data="geoList" :pagination="false" class="h-100%" :scroll="{ y: '100%' }">
<template #empty>
<NoData />
</template>
<template #columns>
<a-table-column title="排名" data-index="rank" />
<a-table-column title="城市" data-index="geo" />
<a-table-column title="分布占比" data-index="rate">
<template #cell="{ record }">
<span class="cts">{{ (record.rate * 100).toFixed(2) }}%</span>
</template>
</a-table-column>
<a-table-column title="TGI指数" data-index="tgi" />
</template>
</a-table>
</a-tab-pane>
</a-tabs>
</div>
</div>
</div>
</view>
</template>
<script setup>
import topHeader from './topHeader.vue';
import { fetchAgeDistributionsList, fetchGeoDistributionsList, fetchGenderDistributionsList } from '@/api/all/index';
import { ref, onMounted, computed } from 'vue';
import * as echarts from 'echarts';
import chinaJson from '@/assets/maps/china.json';
echarts.registerMap('china', chinaJson);
const scope = ref(1); // 地域范围1-省2-市
const chartInstance = (ref < echarts.ECharts) | (null > null);
const topHeaderRef = ref();
// 从topHeader获取统一的状态
const selectedIndustry = computed(() => topHeaderRef.value?.selectedIndustry);
const selectedSubCategory = computed(() => topHeaderRef.value?.selectedSubCategory);
const selectedTimePeriod = computed(() => topHeaderRef.value?.selectedTimePeriod);
const genderData = ref([]);
const genderValueData = ref([]);
const ageValueData = ref([]);
const geoList = ref([]);
// 监听筛选条件变化
watch([selectedIndustry, selectedTimePeriod, selectedSubCategory], () => {
getAgeDistributionsList();
getGeoDistributionsList();
getGenderDistributionsList();
drawChinaMap();
});
const search = () => {
getAgeDistributionsList();
getGeoDistributionsList();
getGenderDistributionsList();
drawChinaMap();
};
// 获取年龄分布列表
const getAgeDistributionsList = async () => {
const params = {
industry_id: selectedIndustry.value,
time_dimension: selectedTimePeriod.value,
};
if (selectedIndustry.value == undefined) {
return;
}
if (selectedSubCategory.value == undefined) {
return;
}
if (selectedSubCategory.value != 0) {
params['industry_id'] = selectedSubCategory.value;
}
const { code, data } = await fetchAgeDistributionsList(params);
if (code === 200) {
ageValueData.value = data;
nextTick(() => {
drawAgeChart();
});
}
};
// 获得地理分布列表
const getGeoDistributionsList = async () => {
const params = {
scope: scope.value,
industry_id: selectedIndustry.value,
time_dimension: selectedTimePeriod.value,
};
if (selectedIndustry.value == undefined) {
return;
}
if (selectedSubCategory.value == undefined) {
return;
}
if (selectedSubCategory.value != 0) {
params['industry_id'] = selectedSubCategory.value;
}
const { code, data } = await fetchGeoDistributionsList(params);
if (code === 200) {
geoList.value = data;
}
};
// 获取性别分布列表
const getGenderDistributionsList = async () => {
const params = {
industry_id: selectedIndustry.value,
time_dimension: selectedTimePeriod.value,
};
if (selectedIndustry.value == undefined) {
return;
}
if (selectedSubCategory.value == undefined) {
return;
}
if (selectedSubCategory.value != 0) {
params['industry_id'] = selectedSubCategory.value;
}
const { code, data } = await fetchGenderDistributionsList(params);
if (code === 200) {
genderData.value = [];
genderValueData.value = [];
genderData.value = [...data];
await nextTick();
genderValueData.value = data.map((item) => ({
value: item.rate * 100,
tgi: item.tgi,
name: item.gender === 1 ? '女性' : '男性',
}));
drawChart();
}
};
const drawChart = () => {
let dom = document.getElementById('container');
let myChart = echarts.init(dom, null, {
renderer: 'canvas',
useDirtyRect: false,
});
let option = {
color: ['#F64B31', '#2A59F3'],
tooltip: {
trigger: 'item',
backgroundColor: '#fff',
borderColor: 'rgba(0,0,0,0.05)',
borderWidth: 1,
textStyle: {
color: '#222',
fontSize: 14,
},
extraCssText: 'box-shadow:0 2px 8px 0 rgba(0,0,0,0.08);border-radius:8px;',
formatter: function (params) {
return `
<div class="w-140px">
<div style="font-weight:500;margin-bottom:8px;">${params.name}</div>
<div style="display:flex;align-items:center;margin-bottom:4px;">
<span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:#2a59f3;margin-right:8px;"></span>
<span>占比</span>
<span style="margin-left:auto;">${params.value}%</span>
</div>
<div style="display:flex;align-items:center;">
<span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:#F64B31;margin-right:8px;"></span>
<span>TGI</span>
<span style="margin-left:auto;">${params.data.tgi || '-'}</span>
</div>
</div>
`;
},
},
series: [
{
type: 'pie',
avoidLabelOverlap: false,
data: genderValueData.value,
label: {
show: false, // 关闭扇区外的文字
},
labelLine: {
show: false,
},
radius: ['40%', '55%'],
},
],
};
myChart.setOption(option);
};
const drawAgeChart = () => {
const dom = document.getElementById('age-container');
// 数据准备
const xAxis = ageValueData.value.map((item) => item.age);
const yAxis = ageValueData.value.map((item) => item.rate * 100);
const yAxis2 = ageValueData.value.map((item) => item.tgi);
// const average = yAxis2.reduce((sum, val) => sum + val, 0) / yAxis2.length;
// 图表初始化强制使用2D渲染
const myChart = echarts.init(dom, null, {
renderer: 'canvas',
useDirtyRect: false,
devicePixelRatio: window.devicePixelRatio * 1.5, // 提高渲染精度
});
// 动态计算刻度间隔
const calcInterval = (data, base = 10) => {
const range = Math.max(...data) - Math.min(...data);
return Math.max(base, Math.ceil(range / 4 / base) * base);
};
// 核心配置
const option = {
animation: false, // 小尺寸下关闭动画提升性能
tooltip: {
trigger: 'axis',
confine: true,
axisPointer: {
type: 'shadow',
label: {
fontSize: 12,
backgroundColor: 'rgba(0,0,0,0.7)',
},
},
backgroundColor: '#fff',
borderColor: 'rgba(0,0,0,0.05)',
borderWidth: 1,
textStyle: {
color: '#333',
fontSize: 14,
},
extraCssText: 'box-shadow:0 2px 8px 0 rgba(0,0,0,0.08);border-radius:8px;',
formatter: function (params) {
const name = params[0].name;
const percent = params[0].value;
const tgi = params[1].value;
return `
<div class="w-140px">
<div style="margin-bottom: 4px;" class="color-#000">${name}岁</div>
<div style="display: flex;align-items: center;margin-bottom:2px;">
<span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:#6d4cfe;margin-right:6px;"></span>
<span>占比</span>
<span style="color:#333;margin-left:auto;">${percent.toFixed(2)}%</span>
</div>
<div style="display: flex;align-items: center;">
<span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:#F64B31;margin-right:6px;"></span>
<span>TGI</span>
<span style="color:#333;margin-left:auto;">${tgi}</span>
</div>
</div>
`;
},
},
grid: {
top: 25,
right: 30,
bottom: 40,
left: 40,
containLabel: false,
},
xAxis: {
type: 'category',
data: xAxis,
axisLabel: {
interval: 0,
rotate: 0,
fontSize: 12,
margin: 10,
hideOverlap: true, // 自动隐藏重叠标签
color: '#939499',
},
axisTick: {
alignWithLabel: true,
length: 3, // 缩短刻度线
},
axisLine: {
lineStyle: {
width: 0.5, // 减细轴线
},
},
},
yAxis: [
{
// 左侧百分比轴
type: 'value',
name: '占比',
nameLocation: 'end',
nameGap: 10,
nameTextStyle: {
color: '#939499',
fontSize: 12,
padding: [0, 50, 0, 0], // 微调名称位置
},
min: 0,
max: Math.ceil(Math.max(...yAxis) / 20) * 20,
interval: calcInterval(yAxis, 20),
axisLabel: {
formatter: '{value}%',
fontSize: 12,
margin: 10,
showMinLabel: true,
showMaxLabel: true,
color: '#939499',
},
splitLine: {
lineStyle: {
type: 'dashed',
// color: 'rgba(255,255,255,0.3)',
// width: 0.8,
},
},
},
{
// 右侧TGI轴
type: 'value',
name: 'TGI',
nameLocation: 'end',
nameGap: 10,
nameTextStyle: {
fontSize: 12,
color: '#939499',
padding: [0, 0, 0, 20],
},
min: Math.floor(Math.min(...yAxis2) / 50) * 50,
max: Math.ceil(Math.max(...yAxis2) / 50) * 50,
interval: calcInterval(yAxis2, 50),
axisLabel: {
fontSize: 12,
margin: 4,
color: '#939499',
},
splitLine: {
lineStyle: {
type: 'dashed',
// color: 'rgba(255,255,255,0.3)',
// width: 0.8,
},
},
axisLine: {
lineStyle: {
type: 'dashed',
color: '#F64B31',
width: 0.8,
},
},
},
],
series: [
{
// 柱状图
name: '占比',
type: 'bar',
barWidth: 16,
itemStyle: {
color: '#6d4cfe',
borderRadius: [1, 1, 0, 0], // 微圆角
},
label: {
show: true,
position: 'top',
formatter: '{c}%',
fontSize: 12,
color: '#fff',
distance: 1, // 减小标签与柱子的距离
},
data: yAxis,
},
{
// 折线图
name: 'TGI',
type: 'line',
yAxisIndex: 1,
symbol: 'none',
lineStyle: {
color: '#F64B31',
width: 1.8,
},
markLine: {
silent: true,
symbol: 'none',
lineStyle: {
color: '#FFAE00',
width: 1.2,
type: 'dashed',
},
label: {
fontSize: 12,
color: '#FFAE00',
formatter: (params) => {
// 改用回调函数
const avg = params.data.coord[1]; // 获取平均值坐标
return 'TGI:' + avg.toFixed(0); // 格式化显示
},
position: 'insideEnd',
offset: [0, -10],
},
data: [{ type: 'average' }],
},
data: yAxis2,
},
],
};
// 应用配置
myChart.setOption(option);
// 超小尺寸适配
const handleResize = () => {
const currentWidth = dom.clientWidth;
if (currentWidth < 400) {
myChart.setOption({
xAxis: { axisLabel: { rotate: 90, fontSize: 7 } },
grid: { bottom: 35 },
});
}
myChart.resize();
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
};
const tabChange = (val) => {
scope.value = val;
getGeoDistributionsList();
};
const drawChinaMap = () => {
const dom = document.getElementById('chinaMap');
// 图表初始化强制使用2D渲染
const myChart = echarts.init(dom, null, {
renderer: 'canvas',
useDirtyRect: false,
devicePixelRatio: window.devicePixelRatio * 1.5, // 提高渲染精度
});
const option = {
series: [
{
name: '中国',
type: 'map',
map: 'china',
label: {
show: false,
},
data: [
{ name: '北京', value: 1000 },
{ name: '上海', value: 800 },
{ name: '广东', value: 900 },
// 其他省份数据...
],
},
],
};
myChart.setOption(option);
};
onMounted(() => {
getAgeDistributionsList();
getGeoDistributionsList();
getGenderDistributionsList();
drawChinaMap();
});
</script>
<style scoped lang="scss">
/* 自定义样式 */
:deep(.arco-table-th) {
background-color: var(--color-fill-2);
}
:deep(.arco-table-tr):hover {
background-color: var(--color-fill-1);
}
:deep(.arco-statistic-content .arco-statistic-value-integer) {
font-size: 14px;
}
.pop-btn {
background: #fff !important;
border-color: #fff !important;
color: #737478 !important;
margin-left: -5px;
}
:deep(.arco-btn-outline) {
color: #6d4cfe !important;
border-color: #6d4cfe !important;
}
.title-row {
display: flex;
height: 64px;
padding: 10px 0 2px 0;
align-items: center;
.title {
color: var(--Text-1, #211f24);
font-family: $font-family-medium;
font-size: 16px;
font-style: normal;
font-weight: 400;
line-height: 24px; /* 150% */
}
}
.cts {
color: var(--Text-2, #3c4043);
font-family: $font-family-regular;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 22px; /* 157.143% */
&.num {
font-family: $font-family-manrope-regular;
}
}
:deep(.arco-tabs) {
display: flex;
flex-direction: column;
.arco-tabs-content {
flex: 1;
.arco-tabs-content-list,
.arco-tabs-pane,
.arco-table-container {
height: 100%;
}
}
}
</style>