SocialNetworks_duan/src/views/KeyNodeDiscern/anchorRecommendation/index.vue
2025-08-14 16:12:30 +08:00

722 lines
21 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>
<el-tooltip
class="box-item"
effect="light"
:content="currentTopic.keyuser"
placement="bottom"
>
<div class="label-value">{{ cutOutTheFirstTwo(currentTopic.keyuser) }}</div>
</el-tooltip>
</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">
<div class="leader-name">{{ currentSelectedAnchorItem.nodeName }}</div>
<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>
首次活跃时间:   {{ currentSelectedAnchorItem.earlistTime }}
</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"
import { useKeyNodeRecognitionStore } from "@/store/keyNodeRecognition/index"
const KeyNodeOneStore = useKeyNodeRecognitionStore()
//控制弹窗
const postDialog = ref(false)
const topicDialog = ref(false)
const anchorDialog = ref(false)
//当前选中的贴文数据
const currentPostPost = ref(null)
//当前选中的热度事件
const currentTopic = ref(null)
//当前选中的锚点数据
const currentSelectedAnchorItem = ref(null)
//筛选tabs展示条件
const filterShowUserList = computed(() => {
if (KeyNodeOneStore.currentTabType == "全部") {
return KeyNodeOneStore.mediaData
} else {
return KeyNodeOneStore.mediaData.filter((item) => item.type === KeyNodeOneStore.currentTabType)
}
})
const contentType = computed(() => {
return (content) => {
if (content.startsWith("//@")) return "转发"
return "原发"
}
})
//直接截取前两个关键用户
const cutOutTheFirstTwo = computed(() => {
return (keyUserString) => {
return keyUserString.split(",").slice(0, 2).join(",")
}
})
watch(filterShowUserList, (newList) => {
if (!newList?.length) return
const { anchorChartInfoList } = KeyNodeOneStore
const find = (name) => anchorChartInfoList.find((item) => item.name === name)
// 安全数值转换函数
const safeNumber = (val) => (isNaN(+val) ? 0 : +val)
// 更新各项数据
find("锚点数量").number = newList.length
find("平均粉丝数量").number = `${(
newList.reduce((acc, cur) => acc + safeNumber(cur.number?.replace("w", "")), 0) / newList.length
).toFixed(1)}w`
find("平均发帖频率").number = `${Math.round(
newList.reduce((acc, cur) => acc + safeNumber(cur.transmit?.replace("h/1次", "")), 0) /
newList.length
)}h/1次`
find("平均参与互动次数").number = Math.floor(
newList.reduce((acc, cur) => acc + safeNumber(cur.interaction), 0) / newList.length
)
})
const handleSwitchTab = (tabName) => {
KeyNodeOneStore.currentTabType = tabName
}
const handleSwitchChartTab = (tabName) => {
KeyNodeOneStore.currentChartTabType = tabName
}
const handlePostDialog = (post) => {
postDialog.value = true
currentPostPost.value = post
}
const handleGotTopicDialog = (topic) => {
topicDialog.value = true
currentTopic.value = topic
}
let hotChartInstance = null
const renderHotChart = () => {
nextTick(() => {
const chartDom = document.getElementById("hotChart")
if (!chartDom) return
if (hotChartInstance) {
hotChartInstance.dispose()
}
hotChartInstance = echarts.init(chartDom)
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"]
},
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)" },
label: { show: true, position: "bottom", color: "#4AC6FF", fontSize: 12 },
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)
})
}
const handleOpenAnchorDialog = (params) => {
anchorDialog.value = params.anchorDialog
currentSelectedAnchorItem.value = params.currentSelectAnchorNode
console.log(currentSelectedAnchorItem.value)
}
watch(topicDialog, (val) => {
if (val) {
renderHotChart()
} else {
if (hotChartInstance) {
hotChartInstance.dispose()
hotChartInstance = null
}
}
})
</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%;
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%;
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%;
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;
cursor: pointer;
}
}
}
}
}
}
}
.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>