SocialNetworks_duan/src/views/KeyNodeDiscern/anchorRecommendation/index.vue

722 lines
21 KiB
Vue
Raw Normal View History

2025-07-17 10:28:56 +08:00
<template>
<div class="keyNode3-container">
<div class="top-container"></div>
<div class="content">
<div class="left-container">
<div class="userPanel">
<UserPanel
:tabsSwitch="KeyNodeOneStore.tabsSwitch"
:tabsList="filterShowUserList"
@click:switchTab="handleSwitchTab"
></UserPanel>
</div>
<div class="anchorChart">
<UserChart
:anchorInfoList="KeyNodeOneStore.anchorChartInfoList"
:chartTab="['注册时间', '行为模式']"
@click:switchChartTab="handleSwitchChartTab"
></UserChart>
</div>
</div>
<div class="middle-container">
<div class="anchorGraph">
<AnchorGraph
:statisticsList="KeyNodeOneStore.statisticsList"
@click:openAnchorDialog="handleOpenAnchorDialog"
></AnchorGraph>
</div>
<div class="anchorGrap-statistics">
<AnchorPost
:posts="KeyNodeOneStore.posts"
@click:openDialog="handlePostDialog"
></AnchorPost>
</div>
</div>
<div class="right-container">
<div class="attention-topic">
<AttentionTopic
:topicList="KeyNodeOneStore.tooltipList"
@click:hotTopic="handleGotTopicDialog"
></AttentionTopic>
</div>
<div class="monitoring-situation">
<Monitoring :anchorMonitorList="KeyNodeOneStore.anchorMonitorList"></Monitoring>
</div>
</div>
</div>
<el-dialog v-model="postDialog" width="640" align-center class="custom-dialog">
<img src="@/assets/images/head/post-dialog-title.png" alt="" class="postTitleImage" />
<div class="dialog-content">
<div class="post-content">{{ currentPostPost.content }}</div>
<div class="heat">
<div class="item-heat-detail">
<div class="item-heat-like">
<Icon icon="ei:like" width="25" height="25" /> {{ currentPostPost.like }}
</div>
<div class="item-heat-comment">
<Icon icon="la:comment-dots" width="25" height="25" /> {{ currentPostPost.comment }}
</div>
<div class="item-heat-transmit">
<Icon icon="mdi:share-outline" width="25" height="25" /> {{
currentPostPost.transmit
}}
</div>
</div>
</div>
</div>
</el-dialog>
<el-dialog v-model="topicDialog" width="591" align-center class="custom-dialog">
<img src="@/assets/images/head/anchorTopicDetail.png" alt="" class="postTitleImage" />
<div class="dialog-content topic-dialog-content">
<div class="dialog-title">{{ currentTopic.title }}</div>
<div class="event-forecast-container">
<div class="event-title">
<img src="@/assets/images/icon/eventTitleLeftIcon.png" alt="" />
<div class="title-font">事件热度预测</div>
</div>
<div class="event-hotChart-container" id="hotChart"></div>
</div>
<div class="event-keyUser-container">
<div class="event-title">
<img src="@/assets/images/icon/eventTitleLeftIcon.png" alt="" />
<div class="title-font">事件关键用户</div>
</div>
<div class="key-user-info-list">
<div class="info-item">
<div class="diamond"></div>
<div class="user-label">
<div class="label-key">最初首发者</div>
<div class="label-value">{{ currentTopic.earler }}</div>
</div>
</div>
<div class="info-item">
<div class="info-item">
<div class="diamond"></div>
<div class="user-label">
<div class="label-key">积极转发者</div>
<div class="label-value">{{ currentTopic.switcher }}</div>
</div>
</div>
</div>
<div class="info-item">
<div class="info-item">
<div class="diamond"></div>
<div class="user-label">
<div class="label-key">积极评论者</div>
<div class="label-value">{{ currentTopic.comenter }}</div>
</div>
</div>
</div>
<div class="info-item">
<div class="info-item">
<div class="diamond"></div>
<div class="user-label">
<div class="label-key">高效扩散者</div>
<div class="label-value">梨涡远点</div>
</div>
</div>
</div>
<div class="info-item">
<div class="info-item">
<div class="diamond"></div>
<div class="user-label">
<div class="label-key">锚点用户</div>
2025-08-14 14:42:11 +08:00
<el-tooltip
class="box-item"
effect="light"
:content="currentTopic.keyuser"
placement="bottom"
>
<div class="label-value">{{ cutOutTheFirstTwo(currentTopic.keyuser) }}</div>
</el-tooltip>
2025-07-17 10:28:56 +08:00
</div>
</div>
</div>
</div>
</div>
</div>
</el-dialog>
<el-dialog v-model="anchorDialog" width="640" align-center class="custom-dialog">
<img src="@/assets/images/head/anchorDialogTitle.png" alt="" class="dialogTitleImg" />
<div class="dialog-content">
<div class="dialog-content-leaderInfo">
<img class="leaderInfo-avatar" :src="currentSelectedAnchorItem.avatar" alt="" />
<div class="leaderInfo-message">
2025-08-14 14:42:11 +08:00
<div class="leader-name">{{ currentSelectedAnchorItem.nodeName }}</div>
2025-07-17 10:28:56 +08:00
<div class="leader-heat">
<div class="fancy">粉丝量&nbsp;&nbsp;{{ currentSelectedAnchorItem.fancy }}</div>
<div class="post-number">
关注量&nbsp;&nbsp;  {{ currentSelectedAnchorItem.atten }}
</div>
</div>
</div>
</div>
<div class="dialog-content-post">
<div class="leader-post-detail-content">
<div
class="content-item"
v-for="item in currentSelectedAnchorItem.postList"
:key="item.id"
>
<div class="item-type">{{ contentType(item.postContent) }}</div>
<div class="item-content">
{{ item.postContent }}
</div>
<div class="item-heat">
<div class="item-time">{{ item.postTime }}</div>
<div class="item-heat-detail">
<div class="item-heat-like">
<Icon icon="ei:like" width="25" height="25" /> {{ item.like }}
</div>
<div class="item-heat-comment">
<Icon icon="la:comment-dots" width="25" height="25" /> {{ item.comment }}
</div>
<div class="item-heat-transmit">
<Icon icon="mdi:share-outline" width="25" height="25" /> {{ item.transmit }}
</div>
</div>
</div>
</div>
</div>
</div>
<div class="dialog-content-heat-degree">
<div class="heat-item">
<p class="diamond"></p>
发帖总数:   {{ currentSelectedAnchorItem.postTotal }}
</div>
<div class="heat-item">
<p class="diamond"></p>
贴文被转总数:   {{ currentSelectedAnchorItem.postTransmitedTotal }}
</div>
<div class="heat-item">
<p class="diamond"></p>
参与互动次数:   {{ currentSelectedAnchorItem.interaction }}
</div>
<div class="heat-item">
<p class="diamond"></p>
2025-07-22 17:40:11 +08:00
首次活跃时间:   {{ currentSelectedAnchorItem.earlistTime }}
2025-07-17 10:28:56 +08:00
</div>
</div>
</div>
</el-dialog>
</div>
</template>
<script setup>
import { computed, ref, watch, nextTick } from "vue"
import UserPanel from "./components/userPanel.vue"
import UserChart from "./components/userChart.vue"
import AnchorGraph from "./components/anchorGraph.vue"
import AnchorPost from "./components/anchorPost.vue"
import AttentionTopic from "./components/attentionTopic.vue"
import Monitoring from "./components/monitoring.vue"
import { Icon } from "@iconify/vue"
import * as echarts from "echarts"
2025-07-17 10:28:56 +08:00
import { useKeyNodeRecognitionStore } from "@/store/keyNodeRecognition/index"
const KeyNodeOneStore = useKeyNodeRecognitionStore()
2025-07-17 10:28:56 +08:00
//控制弹窗
const postDialog = ref(false)
const topicDialog = ref(false)
const anchorDialog = ref(false)
2025-07-17 10:28:56 +08:00
//当前选中的贴文数据
const currentPostPost = ref(null)
2025-07-17 10:28:56 +08:00
//当前选中的热度事件
const currentTopic = ref(null)
2025-07-17 10:28:56 +08:00
//当前选中的锚点数据
const currentSelectedAnchorItem = ref(null)
2025-07-17 10:28:56 +08:00
//筛选tabs展示条件
const filterShowUserList = computed(() => {
if (KeyNodeOneStore.currentTabType == "全部") {
return KeyNodeOneStore.mediaData
2025-07-17 10:28:56 +08:00
} else {
return KeyNodeOneStore.mediaData.filter((item) => item.type === KeyNodeOneStore.currentTabType)
2025-07-17 10:28:56 +08:00
}
})
2025-07-17 10:28:56 +08:00
const contentType = computed(() => {
return (content) => {
if (content.startsWith("//@")) return "转发"
return "原发"
}
})
2025-07-17 10:28:56 +08:00
//直接截取前两个关键用户
const cutOutTheFirstTwo = computed(() => {
return (keyUserString) => {
return keyUserString.split(",").slice(0, 2).join(",")
}
})
2025-07-17 10:28:56 +08:00
watch(filterShowUserList, (newList) => {
if (!newList?.length) return
2025-07-17 10:28:56 +08:00
const { anchorChartInfoList } = KeyNodeOneStore
const find = (name) => anchorChartInfoList.find((item) => item.name === name)
2025-07-17 10:28:56 +08:00
// 安全数值转换函数
const safeNumber = (val) => (isNaN(+val) ? 0 : +val)
2025-07-17 10:28:56 +08:00
// 更新各项数据
find("锚点数量").number = newList.length
2025-07-17 10:28:56 +08:00
find("平均粉丝数量").number = `${(
newList.reduce((acc, cur) => acc + safeNumber(cur.number?.replace("w", "")), 0) / newList.length
).toFixed(1)}w`
2025-07-17 10:28:56 +08:00
find("平均发帖频率").number = `${Math.round(
newList.reduce((acc, cur) => acc + safeNumber(cur.transmit?.replace("h/1次", "")), 0) /
newList.length
)}h/1`
2025-07-17 10:28:56 +08:00
find("平均参与互动次数").number = Math.floor(
newList.reduce((acc, cur) => acc + safeNumber(cur.interaction), 0) / newList.length
)
})
2025-07-17 10:28:56 +08:00
const handleSwitchTab = (tabName) => {
KeyNodeOneStore.currentTabType = tabName
}
2025-07-17 10:28:56 +08:00
const handleSwitchChartTab = (tabName) => {
KeyNodeOneStore.currentChartTabType = tabName
}
2025-07-17 10:28:56 +08:00
const handlePostDialog = (post) => {
postDialog.value = true
currentPostPost.value = post
}
2025-07-17 10:28:56 +08:00
const handleGotTopicDialog = (topic) => {
topicDialog.value = true
currentTopic.value = topic
}
2025-07-17 10:28:56 +08:00
let hotChartInstance = null
2025-07-17 10:28:56 +08:00
const renderHotChart = () => {
nextTick(() => {
const chartDom = document.getElementById("hotChart")
if (!chartDom) return
2025-07-17 10:28:56 +08:00
if (hotChartInstance) {
hotChartInstance.dispose()
2025-07-17 10:28:56 +08:00
}
hotChartInstance = echarts.init(chartDom)
2025-07-17 10:28:56 +08:00
const option = {
backgroundColor: "transparent",
tooltip: { trigger: "axis" },
legend: {
data: ["实际热度", "预测热度"],
right: 20,
top: 10,
textStyle: { color: "#B6D6F7" }
},
grid: { left: 0, right: 20, top: 50, bottom: 30, containLabel: true },
xAxis: {
type: "category",
boundaryGap: false,
axisLine: { show: false },
axisTick: { show: false },
axisLabel: { color: "#B6D6F7", fontSize: 13 },
data: ["6.25", "6.26", "6.27", "6.28", "6.29", "6.30", "7.1", "7.2"]
2025-07-17 10:28:56 +08:00
},
yAxis: {
type: "value",
name: "数量",
nameLocation: "end",
interval: 2000, // 固定间隔2000
nameTextStyle: {
color: "#B6D6F7",
fontSize: 13,
align: "left",
padding: [0, 0, 10, -50] // 负值让文字更靠左
},
axisLine: { show: false },
axisTick: { show: false },
axisLabel: { color: "#B6D6F7", fontSize: 13, margin: 25 },
splitLine: { show: true, lineStyle: { color: "rgba(182,214,247,0.15)" } }
},
series: [
{
name: "实际热度",
type: "line",
data: currentTopic.value.chart.realityData,
symbol: "circle",
symbolSize: 8,
showSymbol: true,
itemStyle: { color: "#4AC6FF", borderColor: "#fff", borderWidth: 2 },
lineStyle: { color: "#4AC6FF", width: 2 },
areaStyle: { color: "rgba(74,198,255,0.15)" },
2025-08-13 16:43:36 +08:00
label: { show: true, position: "bottom", color: "#4AC6FF", fontSize: 12 },
2025-07-17 10:28:56 +08:00
padding: [0, 0, 0, 30]
},
{
name: "预测热度",
type: "line",
data: currentTopic.value.chart.predictedData,
symbol: "circle",
symbolSize: 8,
showSymbol: true,
itemStyle: { color: "#00E5FF", borderColor: "#fff", borderWidth: 2 },
lineStyle: { color: "#00E5FF", width: 2, type: "dashed" },
label: { show: true, position: "top", color: "#00E5FF", fontSize: 12 },
padding: [0, 0, 0, 30]
}
]
}
hotChartInstance.setOption(option)
})
}
2025-07-17 10:28:56 +08:00
const handleOpenAnchorDialog = (params) => {
anchorDialog.value = params.anchorDialog
currentSelectedAnchorItem.value = params.currentSelectAnchorNode
console.log(currentSelectedAnchorItem.value)
}
2025-07-17 10:28:56 +08:00
watch(topicDialog, (val) => {
if (val) {
renderHotChart()
2025-07-17 10:28:56 +08:00
} else {
if (hotChartInstance) {
hotChartInstance.dispose()
hotChartInstance = null
2025-07-17 10:28:56 +08:00
}
}
})
2025-07-17 10:28:56 +08:00
</script>
<style scoped lang="less">
:deep(.custom-dialog) {
width: 640px;
border-width: 0px, 0px, 0px, 0px;
border-style: solid;
border-image-source: linear-gradient(180deg, #3aa1f8 0%, rgba(58, 161, 248, 0.2) 100%);
background-color: rgba(6, 45, 90, 1);
border: 1px solid #1a8bff;
border-radius: 2px;
padding: 0 0;
z-index: 1;
}
:deep(.post-dialog) {
height: 300px;
}
:deep(.custom-dialog) .postTitleImage {
margin-top: -24px;
margin-left: -2px;
}
:deep(.custom-dialog) .dialog-content {
width: 100%;
padding: 25px 20px;
}
:deep(.custom-dialog) .dialog-content .content {
color: rgba(255, 255, 255, 0.8);
font-family: "PingFang SC";
font-size: 16px;
font-style: normal;
font-weight: 400;
opacity: 0.7;
}
.heat {
display: flex;
justify-content: flex-end;
margin-top: 20px;
.item-heat-detail {
display: flex;
justify-content: space-between;
div {
display: flex;
align-items: center;
justify-content: center;
width: 70px;
color: #fff;
opacity: 0.7;
}
}
}
.keyNode3-container {
display: flex;
flex-direction: column;
gap: 20px;
.top-container {
width: 100%;
height: 88px;
background-image: url(@/assets/images/head/bigTitle2.png);
background-repeat: no-repeat;
background-size: cover;
fill: linear-gradient(270deg, rgba(6, 61, 113, 0.1) 0%, rgba(8, 30, 56, 0.38) 100%);
stroke-width: 1px;
stroke: #3aa1f8;
}
.content {
height: 805px;
display: flex;
justify-content: space-between;
gap: 15px;
.left-container {
flex: 10%;
2025-07-17 10:28:56 +08:00
display: flex;
flex-direction: column;
gap: 15px;
.userPanel {
height: 540px;
border-radius: 2px;
flex-shrink: 0;
background-color: rgba(6, 45, 90, 0.3);
border: 1px solid rgba(0, 113, 188, 0.705);
}
.anchorChart {
flex: 1;
border-radius: 2px;
background-color: rgba(6, 45, 90, 0.3);
border: 1px solid rgba(0, 113, 188, 0.705);
overflow: auto;
-ms-overflow-style: none; /* IE 和 Edge */
scrollbar-width: none; /* Firefox */
}
}
.middle-container {
flex: 60%;
2025-07-17 10:28:56 +08:00
display: flex;
flex-direction: column;
gap: 15px;
.anchorGraph {
width: 100%;
height: 541px;
background-color: #070a22;
border-radius: 4px;
background-color: rgba(6, 45, 90, 0.3);
border: 1px solid rgba(0, 113, 188, 0.705);
}
.anchorGrap-statistics {
flex: 1;
border-radius: 4px;
background-color: rgba(6, 45, 90, 0.3);
border: 1px solid rgba(0, 113, 188, 0.705);
fill: linear-gradient(270deg, rgba(6, 61, 113, 0.1) 0%, rgba(8, 30, 56, 0.38) 100%);
stroke-width: 1px;
stroke: #3aa1f8;
overflow: auto;
padding: 10px 10px;
}
}
.right-container {
flex: 25%;
2025-07-17 10:28:56 +08:00
height: 100%;
display: flex;
flex-direction: column;
gap: 15px;
.attention-topic {
width: 100%;
height: 540px;
border-radius: 2px;
flex-shrink: 0;
background-color: rgba(6, 45, 90, 0.3);
border: 1px solid rgba(0, 113, 188, 0.705);
}
.monitoring-situation {
flex: 1;
border-radius: 2px;
flex-shrink: 0;
background-color: rgba(6, 45, 90, 0.3);
border: 1px solid rgba(0, 113, 188, 0.705);
}
}
}
.topic-dialog-content {
.dialog-title {
color: #fff;
font-family: "PingFang SC";
font-size: 20px;
font-style: normal;
}
.event-title {
display: flex;
align-items: center;
.title-font {
margin-left: 8px;
color: #fff;
font-family: "PingFang SC";
font-size: 16px;
font-style: normal;
font-weight: 400;
}
}
.event-forecast-container {
width: 100%;
height: 300px;
margin-top: 22px;
.event-hotChart-container {
width: 100%;
height: 400px;
margin-top: 10px;
}
}
.event-keyUser-container {
margin-top: 150px;
.key-user-info-list {
margin-top: 20px;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
.info-item {
display: flex;
align-items: center;
.diamond {
width: 6px;
height: 6px;
background-color: #fff;
margin-right: 10px;
box-shadow: 0 4px 8px rgb(0, 123, 255);
}
.user-label {
display: flex;
align-items: center;
.label-key {
color: #c6e3f5;
font-family: "PingFang SC";
font-size: 16px;
font-style: normal;
font-weight: 400;
}
.label-value {
width: 170px;
color: #fff;
font-family: "PingFang SC";
font-size: 16px;
font-style: normal;
font-weight: 400;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
2025-08-14 14:42:11 +08:00
cursor: pointer;
2025-07-17 10:28:56 +08:00
}
}
}
}
}
}
}
.dialogTitleImg {
margin-top: -23px;
}
.post-content {
color: #fff;
font-size: 16px;
opacity: 0.7;
}
.dialog-content-leaderInfo {
width: 100%;
height: 70px;
display: flex;
}
.leaderInfo-avatar {
width: 70px;
height: 70px;
border-radius: 5px;
}
.leaderInfo-message {
flex: 1;
height: 100%;
padding-left: 15px;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.leader-name {
font-size: 20px;
color: #fff;
}
.leader-heat {
display: flex;
color: #fff;
font-size: 16px;
}
.post-number {
margin-left: 30px;
}
.dialog-content-post {
width: 100%;
padding: 25px 0px;
}
.leader-post-detail-content {
width: 100%;
background:
linear-gradient(0deg, #0d2743, #0d2743),
linear-gradient(270deg, rgba(147, 210, 255, 0.06) 0%, rgba(147, 210, 255, 0.16) 100%);
margin-top: 30px;
height: 262px;
overflow: auto;
padding: 10px 20px;
}
.leader-post-detail-content::-webkit-scrollbar {
width: 5px;
height: 5px;
}
.leader-post-detail-content::-webkit-scrollbar-thumb {
background: rgba(147, 210, 255, 0.3);
border-radius: 4px;
}
.leader-post-detail-content::-webkit-scrollbar-thumb:hover {
background: rgba(147, 210, 255, 0.5);
}
.item-type {
font-size: 16px;
color: #ffffff;
}
.item-content {
color: #ffffffcc;
font-size: 16px;
}
.content-item div {
margin-bottom: 10px;
}
.item-heat {
display: flex;
justify-content: space-between;
color: #ffffffcc;
}
.item-heat-detail {
display: flex;
}
.item-heat-detail div {
width: 60px;
margin-right: 20px;
display: flex;
align-items: center;
}
.dialog-content-heat-degree {
display: grid;
grid-template-columns: 1fr 1fr;
row-gap: 15px;
color: #fff;
font-size: 16px;
}
.heat-item {
display: flex;
align-items: center;
}
.heat-item .diamond {
width: 6px;
height: 6px;
background-color: #fff;
margin-right: 10px;
box-shadow: 0 4px 8px rgb(0, 123, 255);
}
</style>