SocialNetworks_duan/src/views/GroupEvolution/component/groupPost.vue
2025-08-06 15:53:13 +08:00

377 lines
9.4 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>
<div class="postList-component">
<img
:src="isAbnormal ? abnormalTitle : groupEvolutionTitle"
alt=""
style="margin-top: -17px; margin-left: -11px"
/>
<div
class="post-list-wrapper"
ref="listRef"
@mouseenter="pauseScroll"
@mouseleave="resumeScroll"
>
<div v-if="!isAbnormal" class="scrolling-content">
<div
class="post-item"
v-for="(post, index) in posts"
:key="index"
:class="{ highlighted: post.highlighted }"
@click="handleLeaderPost(post)"
>
<div class="post-type">
<img src="@/assets/images/groupEvolution/group_icon.png" alt="" />
</div>
<span class="timestamp">{{ TansTimestamp(post.time, "YYYY.MM.DD HH:mm:ss") }}</span>
<span class="author" v-if="props.moduleName == '群体识别发现'"
>【{{ post.type }}】&nbsp;&nbsp;的节点数{{ post.nodeChange < 0 ? "减少" : "增加"
}}<span style="color: #9df9fe">{{ Math.abs(post.nodeChange) }}</span
>个,&nbsp;&nbsp;连边{{ post.edgeChange < 0 ? "减少" : "增加"
}}<span style="color: #9df9fe">{{ Math.abs(post.edgeChange) }}</span
>个
</span>
<span class="author" v-else-if="props.moduleName == '群体结构演化分析'"
>【{{ post.groupCategory }}】&nbsp;&nbsp;的内部连边增加<span style="color: #9df9fe">{{
post.innerEdgeAddCount
}}</span
>条,&nbsp;&nbsp;外部连边增加<span style="color: #9df9fe">{{
post.outerEdgeAddCount
}}</span
>条
</span>
<span class="author" v-else-if="props.moduleName == '群体成员演化分析'"
>【{{ post.type }}】&nbsp;&nbsp;的成员数目{{ post.memberAddCount < 0 ? "减少" : "增加"}}了<span style="color: #9df9fe">{{
Math.abs(post.memberAddCount)
}}</span
>位
</span>
</div>
</div>
<div v-else class="scrolling-content">
<div
v-for="(post, index) in posts"
:key="index"
:class="{ highlighted: post.highlighted }"
@click="handleLeaderPost(post)"
>
<div class="post-type">
<div class="type-left">
<img class="title-img" :src="abnormalPostItemTitle" alt="" />
<span class="title-text">{{ post.abnormalGroup }}</span>
</div>
<div class="type-right">
<div class="right-item">首次发现:{{ post.firstDiscoverTime }}</div>
<div class="right-item">持续时间:{{ post.duration }}</div>
</div>
</div>
<div class="post-explanation">
{{ post.explanation }}
</div>
<div class="post-metrics-list">
<div class="item">
JS散度
<div>滑块组</div>
</div>
<div class="item">
CUSUM
<div>滑块组</div>
</div>
<div class="item">
KS检验值
<div>滑块组</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import abnormalPostItemTitle from "@/assets/images/abnormalGroup/abnormal-action-analysis-item-title.png"
import groupEvolutionTitle from "@/assets/images/groupEvolution/group-evolution-analysis-titlt.png"
import abnormalTitle from "@/assets/images/abnormalGroup/abnormal-action-analysis-title.png"
import { ref, defineProps, defineEmits, onMounted, onBeforeUnmount, watch, nextTick } from "vue"
import { TansTimestamp } from "@/utils/transform"
const props = defineProps({
posts: {
type: Array,
default: () => []
},
isAbnormal: {
type: Boolean,
default: false
},
moduleName: {
type: String,
required: true
}
})
const emit = defineEmits(["click:openDialog"])
const maxInfluence = 10000
const getInfluenceWidth = (influence) => {
const percentage = (influence / maxInfluence) * 100
return `${Math.min(percentage, 100)}%`
}
const handleLeaderPost = (item) => {
emit("click:openDialog", item)
console.log(item)
}
const listRef = ref(null)
let scrollTimer = null
let direction = 1 // 1: 向下, -1: 向上
const scrollStep = 1 // 每步像素
const scrollInterval = 80 // 每40ms滚动一次越大越慢
function startScroll() {
if (scrollTimer) return
scrollTimer = setInterval(() => {
const el = listRef.value
if (!el) return
el.scrollTop += direction * scrollStep
// 到底部
if (el.scrollTop + el.clientHeight >= el.scrollHeight) {
direction = -1
}
// 到顶部
if (el.scrollTop <= 0) {
direction = 1
}
}, scrollInterval)
}
function pauseScroll() {
if (scrollTimer) {
clearInterval(scrollTimer)
scrollTimer = null
}
}
function resumeScroll() {
startScroll()
}
let lastPostsLength = 0 //当列表更新时,记录上一次的长度
watch(
() => props.posts,
(newVal) => {
nextTick(() => {
if (listRef.value && newVal.length > lastPostsLength) {
listRef.value.scrollTop = listRef.value.scrollHeight
}
lastPostsLength = newVal.length //实现按需滚动
})
},
{ deep: true }
)
onMounted(() => {
startScroll()
})
onBeforeUnmount(() => {
pauseScroll()
})
</script>
<style scoped lang="less">
.postList-component {
width: 100%;
height: 100%;
.post-list-wrapper {
width: 100%;
height: 80%;
overflow: auto;
}
/* 滚动条整体样式 - WebKit浏览器 */
.post-list-wrapper::-webkit-scrollbar {
width: 3px; /* 垂直滚动条宽度 */
height: 5px; /* 水平滚动条高度 */
}
/* 滚动条滑块 */
.post-list-wrapper::-webkit-scrollbar-thumb {
background: rgba(147, 210, 255, 0.3); /* 蓝色半透明滑块 */
border-radius: 4px;
}
/* 鼠标悬停在滑块上的效果 */
.post-list-wrapper::-webkit-scrollbar-thumb:hover {
background: rgba(147, 210, 255, 0.5); /* 更明显的蓝色 */
}
.scrolling-content {
display: flex;
flex-direction: column;
gap: 1px;
animation-name: scroll-up;
animation-timing-function: linear;
animation-iteration-count: infinite;
}
.post-list-wrapper:hover .scrolling-content {
animation-play-state: paused;
}
.post-item {
display: flex;
align-items: center;
height: 36px;
padding: 0 16px;
background: linear-gradient(90deg, rgba(63, 169, 245, 0.16) 0%, rgba(0, 84, 187, 0) 100%);
border-left: 2px solid #3fa9f5;
color: white;
font-size: 14px;
gap: 12px;
flex-shrink: 0;
cursor: pointer;
margin: 8px 0;
&:hover {
background: linear-gradient(90deg, rgba(63, 169, 245, 0.4) 0%, rgba(0, 84, 187, 0) 100%);
}
}
.post-item .highlighted {
background: linear-gradient(90deg, rgba(63, 169, 245, 0.4) 0%, rgba(0, 84, 187, 0) 100%);
}
.post-type {
display: flex;
align-items: center;
gap: 8px;
flex-shrink: 0;
.type-left {
position: relative;
.title-text {
position: absolute;
top: 0;
left: 0;
color: #8efbff;
font-family: PingFang SC;
font-weight: 400;
font-style: Medium;
font-size: 14px;
}
}
.type-right {
margin-left: 260px;
display: flex;
align-items: center;
color: #fff;
font-family: PingFang SC;
font-weight: 400;
font-style: Medium;
font-size: 14px;
.right-item {
margin-left: 20px;
}
}
}
.post-explanation {
padding-top: 10px;
padding-bottom: 10px;
color: #fff;
font-family: PingFang SC;
font-weight: 400;
font-style: Medium;
font-size: 14px;
}
.post-metrics-list {
display: flex;
color: #fff;
font-family: PingFang SC;
font-weight: 400;
font-style: Regular;
font-size: 14px;
.item {
display: flex;
padding-right: 30px;
}
}
.post-type-text {
color: #8efbff;
font-size: 14px;
font-weight: 400;
text-shadow: 0px 1px 1px rgba(0, 0, 0, 0.25);
}
.timestamp {
flex-shrink: 0;
}
.author {
flex-grow: 1;
text-align: left;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.influence-section {
display: flex;
align-items: center;
gap: 8px;
flex-shrink: 0;
}
.influence-label {
width: 90px;
display: flex;
justify-content: flex-start;
}
.influence-bar {
width: 200px;
height: 10px;
position: relative;
display: flex;
align-items: center;
}
.bar-track {
width: 100%;
height: 2px;
background-color: rgba(208, 222, 238, 0.1);
position: absolute;
}
.bar-fill {
position: absolute;
height: 2px;
background: linear-gradient(270deg, #00f3ff 0%, #00527d 100%);
}
.bar-handle-wrapper {
position: absolute;
top: 50%;
transform: translateY(-50%);
height: 16px;
width: 16px;
display: flex;
justify-content: center;
align-items: center;
}
.bar-handle {
width: 10px;
height: 10px;
border-radius: 50%;
background-color: #e0f1ff;
position: relative;
box-shadow: 0 0 6px 0 rgba(13, 97, 255, 0.8);
}
.bar-handle::before {
content: "";
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 16px;
height: 16px;
border-radius: 50%;
border: 1px solid rgba(21, 154, 255, 0.3);
}
}
</style>